use super::*;
use crate::cfg::build_control_flow_graph;
use crate::dataflow;
use crate::reachability::{block_reaches_unless, block_reaches_without};
use shuck_ast::{
    BourneParameterExpansion, BuiltinCommand, CompoundCommand, ParameterExpansionSyntax, Pattern,
    PatternPart, StmtSeq, StmtTerminator, Word, WordPart, WordPartNode, ZshExpansionTarget,
    static_word_text,
};

impl<'model> SemanticAnalysis<'model> {
    pub(crate) fn new(model: &'model SemanticModel) -> Self {
        Self {
            model,
            cfg: OnceLock::new(),
            exact_variable_dataflow: OnceLock::new(),
            #[cfg(test)]
            dataflow: OnceLock::new(),
            unused_assignments: OnceLock::new(),
            unused_assignments_shellcheck_compat: OnceLock::new(),
            uninitialized_references: OnceLock::new(),
            uninitialized_reference_certainties: OnceLock::new(),
            dead_code: OnceLock::new(),
            unreachable_blocks: OnceLock::new(),
            binding_block_index: OnceLock::new(),
            overwritten_functions: OnceLock::new(),
            unreached_functions: OnceLock::new(),
            unreached_functions_shellcheck_compat: OnceLock::new(),
            scope_provided_binding_index: OnceLock::new(),
        }
    }

    /// Returns the lazily built control-flow graph for the underlying semantic model.
    pub fn cfg(&self) -> &ControlFlowGraph {
        self.cfg.get_or_init(|| {
            build_control_flow_graph(
                &self.model.recorded_program,
                &self.model.command_bindings,
                &self.model.command_references,
                &self.model.scopes,
                &self.model.bindings,
                &self.model.call_sites,
                self.model.visible_function_call_bindings(),
            )
        })
    }

    /// Returns the CFG's unreachable blocks as an indexed set.
    #[doc(hidden)]
    pub fn unreachable_blocks(&self) -> &FxHashSet<BlockId> {
        self.unreachable_blocks
            .get_or_init(|| self.cfg().unreachable().iter().copied().collect())
    }

    /// Returns whether a CFG block is unreachable.
    #[doc(hidden)]
    pub fn block_is_unreachable(&self, block_id: BlockId) -> bool {
        self.unreachable_blocks().contains(&block_id)
    }

    /// Returns the function binding resolved for the call whose callee token spans `name_span`.
    pub fn visible_function_binding_at_call(
        &self,
        name: &Name,
        name_span: Span,
    ) -> Option<BindingId> {
        self.model
            .call_sites_for(name)
            .iter()
            .find(|site| site.name_span == name_span)?;

        self.visible_function_call_bindings()
            .get(&SpanKey::new(name_span))
            .copied()
    }

    /// Iterates call sites for `name` that resolved to a visible function binding.
    pub fn resolved_function_call_sites<'analysis>(
        &'analysis self,
        name: &'analysis Name,
    ) -> impl Iterator<Item = (&'analysis CallSite, BindingId)> + 'analysis {
        self.model.call_sites_for(name).iter().filter_map(|site| {
            self.visible_function_call_bindings()
                .get(&SpanKey::new(site.name_span))
                .copied()
                .map(|binding| (site, binding))
        })
    }

    /// Iterates call sites for `name` using resolution suitable for arity-style checks.
    ///
    /// When the main visible-function index has no answer for a call site, this falls back to a
    /// lexical same-name binding search so callers can still reason about nearby definitions.
    pub fn function_call_arity_sites<'analysis>(
        &'analysis self,
        name: &'analysis Name,
    ) -> impl Iterator<Item = (&'analysis CallSite, BindingId)> + 'analysis {
        self.model.call_sites_for(name).iter().filter_map(|site| {
            self.visible_function_call_bindings()
                .get(&SpanKey::new(site.name_span))
                .copied()
                .or_else(|| self.lexical_function_binding_for_call_offset(name, site))
                .map(|binding| (site, binding))
        })
    }

    /// Returns the function body scope owned by `binding_id`, if it is a function definition.
    pub fn function_scope_for_binding(&self, binding_id: BindingId) -> Option<ScopeId> {
        self.model
            .recorded_program
            .function_body_scopes
            .get(&binding_id)
            .copied()
    }

    /// Returns whether `scope` is a function nested inside another function.
    pub fn function_scope_is_nested(&self, scope: ScopeId) -> bool {
        self.model
            .scope(scope)
            .parent
            .and_then(|parent| self.model.enclosing_function_scope(parent))
            .is_some()
    }

    /// Returns top-level `case "$1"` dispatch edges that select function scopes by name.
    pub fn case_cli_dispatches(&self, file: &File, source: &str) -> Vec<CaseCliDispatch> {
        let mut dispatches = Vec::new();

        for pair in file.body.as_slice().windows(2) {
            let [case_stmt, trailing_exit_stmt] = pair else {
                continue;
            };
            let Command::Compound(CompoundCommand::Case(case_command)) = &case_stmt.command else {
                continue;
            };
            if case_subject_variable_name(&case_command.word) != Some("1") {
                continue;
            }
            if !stmt_is_standalone_exit(trailing_exit_stmt) {
                continue;
            }

            for item in &case_command.cases {
                let Some(dispatcher_span) = first_positional_dispatch_in_commands(&item.body)
                else {
                    continue;
                };

                for pattern in &item.patterns {
                    let Some(name) = static_case_pattern_text(pattern, source) else {
                        continue;
                    };
                    if !is_plausible_shell_function_name(&name) {
                        continue;
                    }

                    let name = Name::from(name.as_str());
                    let Some(binding_id) = self.visible_function_binding_defined_before(
                        &name,
                        self.model.scope_at(dispatcher_span.start.offset()),
                        dispatcher_span.start.offset(),
                    ) else {
                        continue;
                    };
                    let Some(scope) = self.function_scope_for_binding(binding_id) else {
                        continue;
                    };

                    dispatches.push(CaseCliDispatch::new(scope, dispatcher_span));
                }
            }
        }

        dispatches
    }

    /// Returns function scopes that participate in a top-level `case "$1"` CLI dispatch pattern.
    pub fn case_cli_reachable_function_scopes(
        &self,
        file: &File,
        dispatches: &[CaseCliDispatch],
    ) -> Vec<ScopeId> {
        let dispatcher_offset = dispatches
            .iter()
            .map(|dispatch| dispatch.dispatcher_span().start.offset())
            .min();
        let top_level_exit_offset = file
            .body
            .iter()
            .find(|stmt| stmt_is_standalone_exit(stmt))
            .map(|stmt| stmt.span.start.offset());
        let mut scopes = self
            .function_bindings_by_scope()
            .filter_map(|(scope, bindings)| {
                let function_start = bindings
                    .iter()
                    .map(|binding_id| self.model.binding(*binding_id).span.start.offset())
                    .min()?;
                (self.function_scope_is_nested(scope)
                    || dispatcher_offset.is_some_and(|offset| function_start < offset)
                    || top_level_exit_offset.is_some_and(|offset| function_start < offset))
                .then_some(scope)
            })
            .collect::<Vec<_>>();
        scopes.sort_by_key(|scope| self.model.scope(*scope).span.start.offset());
        scopes.dedup();
        scopes
    }

    /// Returns the latest visible same-name function binding defined before the given call site.
    pub fn visible_function_binding_defined_before(
        &self,
        name: &Name,
        site_scope: ScopeId,
        site_offset: usize,
    ) -> Option<BindingId> {
        self.model
            .ancestor_scopes(site_scope)
            .find_map(|scope| self.latest_function_binding_before(name, scope, site_offset))
    }

    fn lexical_function_binding_for_call_offset(
        &self,
        name: &Name,
        site: &CallSite,
    ) -> Option<BindingId> {
        let scopes = self.model.ancestor_scopes(site.scope).collect::<Vec<_>>();

        scopes
            .iter()
            .copied()
            .find_map(|scope| {
                self.latest_function_binding_before(name, scope, site.name_span.start.offset())
            })
            .or_else(|| {
                scopes
                    .iter()
                    .copied()
                    .find_map(|scope| self.earliest_function_binding_in_scope(name, scope))
            })
    }

    fn latest_function_binding_before(
        &self,
        name: &Name,
        scope: ScopeId,
        offset: usize,
    ) -> Option<BindingId> {
        self.model
            .function_definitions(name)
            .iter()
            .copied()
            .filter(|candidate| self.model.binding(*candidate).scope == scope)
            .filter(|candidate| self.model.binding(*candidate).span.start.offset() < offset)
            .max_by_key(|candidate| self.model.binding(*candidate).span.start.offset())
    }

    fn earliest_function_binding_in_scope(&self, name: &Name, scope: ScopeId) -> Option<BindingId> {
        self.model
            .function_definitions(name)
            .iter()
            .copied()
            .filter(|candidate| self.model.binding(*candidate).scope == scope)
            .min_by_key(|candidate| self.model.binding(*candidate).span.start.offset())
    }

    fn visible_function_call_bindings(&self) -> &FxHashMap<SpanKey, BindingId> {
        self.model.visible_function_call_bindings()
    }

    #[doc(hidden)]
    pub fn function_bindings_by_scope(&self) -> impl Iterator<Item = (ScopeId, &[BindingId])> + '_ {
        self.model
            .function_binding_scope_index()
            .iter()
            .map(|(scope, bindings)| (*scope, bindings.as_slice()))
    }

    /// Creates a direct-call reachability engine over this analysis, optionally seeding extra
    /// synthetic call candidates.
    pub fn direct_function_call_reachability<'analysis>(
        &'analysis self,
        supplemental_calls: impl IntoIterator<Item = FunctionCallCandidate>,
    ) -> DirectFunctionCallReachability<'analysis, 'model> {
        DirectFunctionCallReachability::new(self, supplemental_calls)
    }

    #[doc(hidden)]
    pub fn block_ids_for_span(&self, span: Span) -> &[BlockId] {
        self.cfg().block_ids_for_span(span)
    }

    pub(crate) fn exact_variable_dataflow(&self) -> &ExactVariableDataflow {
        self.exact_variable_dataflow.get_or_init(|| {
            let cfg = self.cfg();
            let context = self.model.dataflow_context(cfg);
            dataflow::build_exact_variable_dataflow(&context)
        })
    }

    #[cfg(test)]
    pub(crate) fn dataflow(&self) -> &DataflowResult {
        self.dataflow.get_or_init(|| {
            let cfg = self.cfg();
            let context = self.model.dataflow_context(cfg);
            let exact = self.exact_variable_dataflow();
            dataflow::analyze(&context, exact)
        })
    }

    #[cfg(test)]
    pub(crate) fn materialized_reaching_definitions(&self) -> ReachingDefinitions {
        let cfg = self.cfg();
        let context = self.model.dataflow_context(cfg);
        let exact = self.exact_variable_dataflow();
        dataflow::materialize_reaching_definitions(&context, exact)
    }

    /// Returns the innermost function scope containing `offset`.
    #[doc(hidden)]
    pub fn enclosing_function_scope_at(&self, offset: usize) -> Option<ScopeId> {
        self.model
            .enclosing_function_scope(self.model.scope_at(offset))
    }

    /// Returns whether a binding's scope is `ancestor_scope` or nested below it.
    #[doc(hidden)]
    pub fn binding_is_in_scope_or_descendant(
        &self,
        binding_id: BindingId,
        ancestor_scope: ScopeId,
    ) -> bool {
        self.model
            .scope_is_in_scope_or_descendant(self.model.binding(binding_id).scope, ancestor_scope)
    }

    /// Returns the entry block that covers the common runtime scope of `binding_scopes`.
    #[doc(hidden)]
    pub fn flow_entry_block_for_binding_scopes(
        &self,
        binding_scopes: &[ScopeId],
        reference_offset: usize,
    ) -> BlockId {
        let cfg = self.cfg();
        self.model
            .ancestor_scopes(self.model.scope_at(reference_offset))
            .find_map(|scope| {
                if !matches!(
                    self.model.scope(scope).kind,
                    ScopeKind::Function(_) | ScopeKind::File
                ) {
                    return None;
                }
                binding_scopes
                    .iter()
                    .copied()
                    .all(|binding_scope| {
                        self.model
                            .scope_is_in_scope_or_descendant(binding_scope, scope)
                    })
                    .then(|| cfg.scope_entry(scope))
                    .flatten()
            })
            .unwrap_or_else(|| cfg.entry())
    }

    /// Returns true when every path from `entry` to `target` crosses one of `cover_blocks`.
    #[doc(hidden)]
    pub fn blocks_cover_all_paths_to_block(
        &self,
        entry: BlockId,
        target: BlockId,
        cover_blocks: &FxHashSet<BlockId>,
    ) -> bool {
        let cfg = self.cfg();
        let unreachable = self.unreachable_blocks();
        !block_reaches_unless(cfg, entry, target, |block| {
            cover_blocks.contains(&block) || unreachable.contains(&block)
        })
    }

    /// Returns true when a binding's CFG block dominates a named reference from the relevant
    /// runtime entry block. Same-block ordering remains caller policy because it often depends on
    /// rule-local structural facts.
    #[doc(hidden)]
    pub fn binding_dominates_reference_from_flow_entry(
        &self,
        binding_id: BindingId,
        name: &Name,
        at: Span,
        same_block_dominates: bool,
    ) -> bool {
        let Some(reference_id) = self.reference_id_for_name_at(name, at) else {
            return false;
        };
        let Some(reference_block) = self.block_for_reference_id(reference_id) else {
            return false;
        };
        let Some(binding_block) = self.block_for_binding(binding_id) else {
            return false;
        };
        if binding_block == reference_block {
            return same_block_dominates;
        }

        let binding_scope = self.model.binding(binding_id).scope;
        let entry = self.flow_entry_block_for_binding_scopes(&[binding_scope], at.start.offset());
        self.blocks_cover_all_paths_to_block(
            entry,
            reference_block,
            &FxHashSet::from_iter([binding_block]),
        )
    }

    /// Returns bindings whose values can reach the named use contained in `at`.
    ///
    /// This prefers exact dataflow when a matching reference is available, then falls back to the
    /// latest lexically visible binding.
    pub fn reaching_bindings_for_name(&self, name: &Name, at: Span) -> Vec<BindingId> {
        let cfg = self.cfg();
        let context = self.model.dataflow_context(cfg);
        let exact = self.exact_variable_dataflow();

        if let Some(reference) = self.reference_for_name_at(name, at) {
            let reaching = exact.reaching_bindings_for_reference(&context, reference);
            if !reaching.is_empty() {
                return reaching;
            }
        }

        self.model
            .visible_binding(name, at)
            .map(|binding| vec![binding.id])
            .unwrap_or_default()
    }

    #[doc(hidden)]
    pub fn visible_bindings_bypassing(
        &self,
        name: &Name,
        binding_id: BindingId,
        at: Span,
    ) -> Vec<BindingId> {
        let cfg = self.cfg();
        let exact = self.exact_variable_dataflow();
        let Some(reference) = self.reference_for_name_at(name, at) else {
            return Vec::new();
        };
        let Some(reference_block) = exact.reference_block(reference) else {
            return Vec::new();
        };
        let Some(binding_block) = exact.binding_block(binding_id) else {
            return Vec::new();
        };
        if reference_block == binding_block {
            return Vec::new();
        }

        let unreachable = cfg.unreachable().iter().copied().collect::<FxHashSet<_>>();
        if unreachable.contains(&reference_block) || unreachable.contains(&binding_block) {
            return Vec::new();
        }

        self.model
            .bindings_for(name)
            .iter()
            .copied()
            .filter(|other_binding| *other_binding != binding_id)
            .filter(|other_binding| self.model.binding_visible_at(*other_binding, at))
            .filter_map(|other_binding| {
                exact
                    .binding_block(other_binding)
                    .filter(|other_block| !unreachable.contains(other_block))
                    .filter(|other_block| {
                        block_reaches_without(cfg, *other_block, reference_block, binding_block)
                    })
                    .map(|_| other_binding)
            })
            .collect()
    }

    /// Returns the lazily computed dead-code regions for the script.
    pub fn dead_code(&self) -> &[DeadCode] {
        self.dead_code
            .get_or_init(|| dataflow::analyze_dead_code(self.cfg()))
            .as_slice()
    }

    #[doc(hidden)]
    pub fn scope_provided_bindings(&self, scope: ScopeId) -> &[ProvidedBinding] {
        self.scope_provided_binding_index()
            .provided_bindings_by_scope
            .get(scope.index())
            .map(Box::as_ref)
            .unwrap_or(&[])
    }

    #[doc(hidden)]
    pub fn definite_provider_scopes(&self, name: &Name) -> &[ScopeId] {
        self.scope_provided_binding_index()
            .definite_provider_scopes_by_name
            .get(name)
            .map(Box::as_ref)
            .unwrap_or(&[])
    }

    #[doc(hidden)]
    pub fn summarize_scope_provided_bindings(&self, scope: ScopeId) -> Vec<ProvidedBinding> {
        self.scope_provided_bindings(scope).to_vec()
    }

    pub(crate) fn summarize_scope_provided_functions(
        &self,
        scope: ScopeId,
    ) -> Vec<ProvidedBinding> {
        let cfg = self.cfg();
        let exact = self.exact_variable_dataflow();
        let context = self.model.dataflow_context(cfg);
        dataflow::summarize_scope_provided_functions(&context, exact, scope)
    }

    fn scope_provided_binding_index(&self) -> &ScopeProvidedBindingIndex {
        self.scope_provided_binding_index.get_or_init(|| {
            let cfg = self.cfg();
            let exact = self.exact_variable_dataflow();
            let context = self.model.dataflow_context(cfg);
            let mut provided_bindings_by_scope = Vec::with_capacity(self.model.scopes.len());
            let mut definite_provider_scopes_by_name = FxHashMap::<Name, Vec<ScopeId>>::default();

            for scope in self.model.scopes.iter().map(|scope| scope.id) {
                let provided_bindings =
                    dataflow::summarize_scope_provided_bindings(&context, exact, scope);
                for binding in &provided_bindings {
                    if binding.certainty == ContractCertainty::Definite {
                        definite_provider_scopes_by_name
                            .entry(binding.name.clone())
                            .or_default()
                            .push(scope);
                    }
                }
                provided_bindings_by_scope.push(provided_bindings.into_boxed_slice());
            }

            let definite_provider_scopes_by_name = definite_provider_scopes_by_name
                .into_iter()
                .map(|(name, scopes)| (name, scopes.into_boxed_slice()))
                .collect();

            ScopeProvidedBindingIndex {
                provided_bindings_by_scope,
                definite_provider_scopes_by_name,
            }
        })
    }
}

fn stmt_is_standalone_exit(stmt: &Stmt) -> bool {
    if stmt.negated || matches!(stmt.terminator, Some(StmtTerminator::Background(_))) {
        return false;
    }

    let Command::Builtin(BuiltinCommand::Exit(command)) = &stmt.command else {
        return false;
    };
    command.extra_args.is_empty() && command.assignments.is_empty() && stmt.redirects.is_empty()
}

fn first_positional_dispatch_in_commands(commands: &StmtSeq) -> Option<Span> {
    commands
        .iter()
        .find_map(|stmt| first_positional_dispatch_in_command(&stmt.command))
}

fn first_positional_dispatch_in_command(command: &Command) -> Option<Span> {
    match command {
        Command::Binary(command) => first_positional_dispatch_in_command(&command.left.command)
            .or_else(|| first_positional_dispatch_in_command(&command.right.command)),
        Command::Compound(CompoundCommand::BraceGroup(commands))
        | Command::Compound(CompoundCommand::Subshell(commands)) => {
            first_positional_dispatch_in_commands(commands)
        }
        Command::Compound(CompoundCommand::If(command)) => {
            first_positional_dispatch_in_commands(&command.condition)
                .or_else(|| first_positional_dispatch_in_commands(&command.then_branch))
                .or_else(|| {
                    command
                        .elif_branches
                        .iter()
                        .find_map(|(condition, branch)| {
                            first_positional_dispatch_in_commands(condition)
                                .or_else(|| first_positional_dispatch_in_commands(branch))
                        })
                })
                .or_else(|| {
                    command
                        .else_branch
                        .as_ref()
                        .and_then(first_positional_dispatch_in_commands)
                })
        }
        Command::Compound(CompoundCommand::While(command)) => {
            first_positional_dispatch_in_commands(&command.condition)
                .or_else(|| first_positional_dispatch_in_commands(&command.body))
        }
        Command::Compound(CompoundCommand::Until(command)) => {
            first_positional_dispatch_in_commands(&command.condition)
                .or_else(|| first_positional_dispatch_in_commands(&command.body))
        }
        Command::Compound(CompoundCommand::For(command)) => {
            first_positional_dispatch_in_commands(&command.body)
        }
        Command::Compound(CompoundCommand::Select(command)) => {
            first_positional_dispatch_in_commands(&command.body)
        }
        Command::Compound(CompoundCommand::Case(command)) => command
            .cases
            .iter()
            .find_map(|item| first_positional_dispatch_in_commands(&item.body)),
        Command::Compound(CompoundCommand::Time(command)) => command
            .command
            .as_ref()
            .and_then(|stmt| first_positional_dispatch_in_command(&stmt.command)),
        Command::Compound(CompoundCommand::Conditional(_))
        | Command::Compound(_)
        | Command::Builtin(_)
        | Command::Decl(_)
        | Command::Function(_)
        | Command::AnonymousFunction(_) => None,
        Command::Simple(command) => {
            word_is_plain_positional_parameter(&command.name, "1").then_some(command.name.span)
        }
    }
}

fn word_is_plain_positional_parameter(word: &Word, target: &str) -> bool {
    let [part] = word.parts.as_slice() else {
        return false;
    };

    word_part_is_plain_positional_parameter(&part.kind, target)
}

fn word_part_is_plain_positional_parameter(part: &WordPart, target: &str) -> bool {
    match part {
        WordPart::Variable(name) => name.as_str() == target,
        WordPart::DoubleQuoted { parts, .. } => {
            let [part] = parts.as_slice() else {
                return false;
            };
            word_part_is_plain_positional_parameter(&part.kind, target)
        }
        WordPart::Parameter(parameter) => match &parameter.syntax {
            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access { reference }) => {
                reference.subscript.is_none() && reference.name.as_str() == target
            }
            ParameterExpansionSyntax::Zsh(syntax) => {
                syntax.length_prefix.is_none()
                    && syntax.operation.is_none()
                    && syntax.modifiers.is_empty()
                    && matches!(
                        &syntax.target,
                        ZshExpansionTarget::Reference(reference)
                            if reference.subscript.is_none()
                                && reference.name.as_str() == target
                    )
            }
            _ => false,
        },
        _ => false,
    }
}

fn static_case_pattern_text(pattern: &Pattern, source: &str) -> Option<String> {
    let mut text = String::new();
    for part in &pattern.parts {
        match &part.kind {
            PatternPart::Literal(literal) => text.push_str(literal.as_str(source, part.span)),
            PatternPart::Word(word) => text.push_str(static_word_text(word, source)?.as_ref()),
            PatternPart::AnyString
            | PatternPart::AnyChar
            | PatternPart::CharClass(_)
            | PatternPart::Group { .. } => return None,
        }
    }
    Some(text)
}

fn case_subject_variable_name(word: &Word) -> Option<&str> {
    standalone_variable_name_from_word_parts(&word.parts)
}

fn standalone_variable_name_from_word_parts(parts: &[WordPartNode]) -> Option<&str> {
    let [part] = parts else {
        return None;
    };

    match &part.kind {
        WordPart::Variable(name) => Some(name.as_str()),
        WordPart::Parameter(parameter) => match parameter.bourne() {
            Some(BourneParameterExpansion::Access { reference })
                if reference.subscript.is_none() =>
            {
                Some(reference.name.as_str())
            }
            _ => None,
        },
        WordPart::DoubleQuoted { parts, .. } => standalone_variable_name_from_word_parts(parts),
        _ => None,
    }
}

fn is_plausible_shell_function_name(name: &str) -> bool {
    let Some(first) = name.chars().next() else {
        return false;
    };
    if !matches!(first, 'a'..='z' | 'A'..='Z' | '_') {
        return false;
    }
    if !name
        .chars()
        .all(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-'))
    {
        return false;
    }
    !matches!(
        name,
        "!" | "{"
            | "}"
            | "if"
            | "then"
            | "elif"
            | "else"
            | "fi"
            | "do"
            | "done"
            | "case"
            | "esac"
            | "for"
            | "in"
            | "while"
            | "until"
            | "time"
            | "[["
            | "]]"
            | "function"
            | "select"
            | "coproc"
    )
}
