use std::collections::BTreeSet;

use crate::ast::{Expr, FnDef, Spanned, Stmt, StrPart, TopLevel};

use super::{CheckFinding, FindingSpan, FnSigMap, dotted_name};

pub fn collect_independence_warnings(items: &[TopLevel], fn_sigs: &FnSigMap) -> Vec<CheckFinding> {
    let mut warnings = Vec::new();
    for item in items {
        if let TopLevel::FnDef(fd) = item {
            collect_independence_warnings_in_fn(fd, fn_sigs, &mut warnings);
        }
    }
    warnings
}

pub fn collect_independence_warnings_in(
    items: &[TopLevel],
    fn_sigs: &FnSigMap,
    file: Option<&str>,
) -> Vec<CheckFinding> {
    let mut warnings = collect_independence_warnings(items, fn_sigs);
    if let Some(file) = file {
        for warning in &mut warnings {
            warning.file = Some(file.to_string());
        }
    }
    warnings
}

fn collect_independence_warnings_in_fn(
    fd: &FnDef,
    fn_sigs: &FnSigMap,
    warnings: &mut Vec<CheckFinding>,
) {
    for stmt in fd.body.stmts() {
        match stmt {
            Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
                collect_independence_warnings_in_expr(expr, fd, fn_sigs, warnings);
            }
        }
    }
}

fn collect_independence_warnings_in_expr(
    expr: &Spanned<Expr>,
    fd: &FnDef,
    fn_sigs: &FnSigMap,
    warnings: &mut Vec<CheckFinding>,
) {
    if let Expr::IndependentProduct(items, _) = &expr.node {
        check_independent_product(expr, items, fd, fn_sigs, warnings);
    }

    match &expr.node {
        Expr::FnCall(callee, args) => {
            collect_independence_warnings_in_expr(callee, fd, fn_sigs, warnings);
            for arg in args {
                collect_independence_warnings_in_expr(arg, fd, fn_sigs, warnings);
            }
        }
        Expr::TailCall(boxed) => {
            let (_, args) = boxed.as_ref();
            for arg in args {
                collect_independence_warnings_in_expr(arg, fd, fn_sigs, warnings);
            }
        }
        Expr::BinOp(_, left, right) => {
            collect_independence_warnings_in_expr(left, fd, fn_sigs, warnings);
            collect_independence_warnings_in_expr(right, fd, fn_sigs, warnings);
        }
        Expr::Match { subject, arms, .. } => {
            collect_independence_warnings_in_expr(subject, fd, fn_sigs, warnings);
            for arm in arms {
                collect_independence_warnings_in_expr(&arm.body, fd, fn_sigs, warnings);
            }
        }
        Expr::ErrorProp(inner) | Expr::Attr(inner, _) => {
            collect_independence_warnings_in_expr(inner, fd, fn_sigs, warnings);
        }
        Expr::Constructor(_, Some(inner)) => {
            collect_independence_warnings_in_expr(inner, fd, fn_sigs, warnings);
        }
        Expr::InterpolatedStr(parts) => {
            for part in parts {
                if let StrPart::Parsed(inner) = part {
                    collect_independence_warnings_in_expr(inner, fd, fn_sigs, warnings);
                }
            }
        }
        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
            for item in items {
                collect_independence_warnings_in_expr(item, fd, fn_sigs, warnings);
            }
        }
        Expr::MapLiteral(entries) => {
            for (key, value) in entries {
                collect_independence_warnings_in_expr(key, fd, fn_sigs, warnings);
                collect_independence_warnings_in_expr(value, fd, fn_sigs, warnings);
            }
        }
        Expr::RecordCreate { fields, .. } => {
            for (_, value) in fields {
                collect_independence_warnings_in_expr(value, fd, fn_sigs, warnings);
            }
        }
        Expr::RecordUpdate { base, updates, .. } => {
            collect_independence_warnings_in_expr(base, fd, fn_sigs, warnings);
            for (_, value) in updates {
                collect_independence_warnings_in_expr(value, fd, fn_sigs, warnings);
            }
        }
        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) | Expr::Constructor(_, None) => {}
    }
}

fn check_independent_product(
    product: &Spanned<Expr>,
    items: &[Spanned<Expr>],
    fd: &FnDef,
    fn_sigs: &FnSigMap,
    warnings: &mut Vec<CheckFinding>,
) {
    if items.len() < 2 {
        return;
    }

    let branch_effects: Vec<BTreeSet<String>> = items
        .iter()
        .map(|item| collect_used_effects(item, fn_sigs))
        .collect();

    for left_idx in 0..items.len() {
        for right_idx in left_idx + 1..items.len() {
            let mut conflicting = BTreeSet::new();
            let mut hazard_kinds = BTreeSet::new();
            for left in &branch_effects[left_idx] {
                for right in &branch_effects[right_idx] {
                    if let Some(kind) = effect_hazard(left, right) {
                        conflicting.insert(left.clone());
                        conflicting.insert(right.clone());
                        hazard_kinds.insert(kind.label());
                    }
                }
            }

            if conflicting.is_empty() {
                continue;
            }

            let left_branch = left_idx + 1;
            let right_branch = right_idx + 1;
            let shared = conflicting.into_iter().collect::<Vec<_>>().join(", ");
            let hazard_note = hazard_kinds.into_iter().collect::<Vec<_>>().join(", ");
            let left_effects = describe_effects(&branch_effects[left_idx]);
            let right_effects = describe_effects(&branch_effects[right_idx]);
            warnings.push(CheckFinding {
                line: product.line,
                module: None,
                file: None,
                fn_name: Some(fd.name.clone()),
                message: format!(
                    "Independent product branches {} and {} use potentially conflicting effects [{}] ({}) — independent products may reorder or overlap these effects; keep them sequential or suppress with [[check.suppress]] reason if this independence is intentional",
                    left_branch, right_branch, shared, hazard_note
                ),
                extra_spans: vec![
                    FindingSpan {
                        line: items[left_idx].line,
                        col: 0,
                        len: 0,
                        label: format!("branch {} uses {}", left_branch, left_effects),
                    },
                    FindingSpan {
                        line: items[right_idx].line,
                        col: 0,
                        len: 0,
                        label: format!("branch {} uses {}", right_branch, right_effects),
                    },
                ],
            });
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
enum HazardKind {
    SharedOutput,
    SharedTransport,
    SharedServerLifecycle,
    DiskMutation,
    HttpMutation,
    EnvMutation,
}

impl HazardKind {
    fn label(self) -> &'static str {
        match self {
            HazardKind::SharedOutput => "shared terminal/output hazard",
            HazardKind::SharedTransport => "shared tcp/socket hazard",
            HazardKind::SharedServerLifecycle => "shared server lifecycle hazard",
            HazardKind::DiskMutation => "disk mutation hazard",
            HazardKind::HttpMutation => "http mutation hazard",
            HazardKind::EnvMutation => "environment mutation hazard",
        }
    }
}

fn describe_effects(effects: &BTreeSet<String>) -> String {
    if effects.is_empty() {
        "no effects".to_string()
    } else {
        effects.iter().cloned().collect::<Vec<_>>().join(", ")
    }
}

fn collect_used_effects(expr: &Spanned<Expr>, fn_sigs: &FnSigMap) -> BTreeSet<String> {
    let mut used = BTreeSet::new();
    collect_used_effects_expr(expr, fn_sigs, &mut used);
    used
}

fn collect_used_effects_expr(expr: &Spanned<Expr>, fn_sigs: &FnSigMap, out: &mut BTreeSet<String>) {
    match &expr.node {
        Expr::FnCall(callee, args) => {
            if let Some(callee_name) = dotted_name(callee)
                && let Some((_, _, effects)) = fn_sigs.get(&callee_name)
            {
                for effect in effects {
                    out.insert(effect.clone());
                }
            }
            collect_used_effects_expr(callee, fn_sigs, out);
            for arg in args {
                collect_used_effects_expr(arg, fn_sigs, out);
            }
        }
        Expr::TailCall(boxed) => {
            let (target, args) = boxed.as_ref();
            if let Some((_, _, effects)) = fn_sigs.get(target) {
                for effect in effects {
                    out.insert(effect.clone());
                }
            }
            for arg in args {
                collect_used_effects_expr(arg, fn_sigs, out);
            }
        }
        Expr::BinOp(_, left, right) => {
            collect_used_effects_expr(left, fn_sigs, out);
            collect_used_effects_expr(right, fn_sigs, out);
        }
        Expr::Match { subject, arms, .. } => {
            collect_used_effects_expr(subject, fn_sigs, out);
            for arm in arms {
                collect_used_effects_expr(&arm.body, fn_sigs, out);
            }
        }
        Expr::ErrorProp(inner) | Expr::Attr(inner, _) => {
            collect_used_effects_expr(inner, fn_sigs, out);
        }
        Expr::Constructor(_, Some(inner)) => collect_used_effects_expr(inner, fn_sigs, out),
        Expr::InterpolatedStr(parts) => {
            for part in parts {
                if let StrPart::Parsed(inner) = part {
                    collect_used_effects_expr(inner, fn_sigs, out);
                }
            }
        }
        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
            for item in items {
                collect_used_effects_expr(item, fn_sigs, out);
            }
        }
        Expr::MapLiteral(entries) => {
            for (key, value) in entries {
                collect_used_effects_expr(key, fn_sigs, out);
                collect_used_effects_expr(value, fn_sigs, out);
            }
        }
        Expr::RecordCreate { fields, .. } => {
            for (_, value) in fields {
                collect_used_effects_expr(value, fn_sigs, out);
            }
        }
        Expr::RecordUpdate { base, updates, .. } => {
            collect_used_effects_expr(base, fn_sigs, out);
            for (_, value) in updates {
                collect_used_effects_expr(value, fn_sigs, out);
            }
        }
        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) | Expr::Constructor(_, None) => {}
    }
}

fn effect_hazard(left: &str, right: &str) -> Option<HazardKind> {
    if shared_output_effect(left, right) {
        Some(HazardKind::SharedOutput)
    } else if same_namespace_any(left, right, "Tcp") {
        Some(HazardKind::SharedTransport)
    } else if same_namespace_any(left, right, "HttpServer") {
        Some(HazardKind::SharedServerLifecycle)
    } else if same_namespace_with_mutation(left, right, "Disk", is_disk_mutating) {
        Some(HazardKind::DiskMutation)
    } else if same_namespace_with_mutation(left, right, "Http", is_http_mutating) {
        Some(HazardKind::HttpMutation)
    } else if same_namespace_with_mutation(left, right, "Env", is_env_mutating) {
        Some(HazardKind::EnvMutation)
    } else {
        None
    }
}

fn same_namespace_any(left: &str, right: &str, namespace: &str) -> bool {
    belongs_to_namespace(left, namespace) && belongs_to_namespace(right, namespace)
}

fn shared_output_effect(left: &str, right: &str) -> bool {
    belongs_to_output_namespace(left) && belongs_to_output_namespace(right)
}

fn same_namespace_with_mutation(
    left: &str,
    right: &str,
    namespace: &str,
    is_mutating: fn(&str) -> bool,
) -> bool {
    belongs_to_namespace(left, namespace)
        && belongs_to_namespace(right, namespace)
        && (is_mutating(left) || is_mutating(right))
}

fn belongs_to_namespace(effect: &str, namespace: &str) -> bool {
    effect == namespace || effect.starts_with(&format!("{namespace}."))
}

fn belongs_to_output_namespace(effect: &str) -> bool {
    belongs_to_namespace(effect, "Console") || belongs_to_namespace(effect, "Terminal")
}

fn is_disk_mutating(effect: &str) -> bool {
    [
        "Disk.writeText",
        "Disk.appendText",
        "Disk.delete",
        "Disk.deleteDir",
        "Disk.makeDir",
    ]
    .iter()
    .any(|required| crate::effects::effect_satisfies(effect, required))
}

fn is_http_mutating(effect: &str) -> bool {
    ["Http.post", "Http.put", "Http.patch", "Http.delete"]
        .iter()
        .any(|required| crate::effects::effect_satisfies(effect, required))
}

fn is_env_mutating(effect: &str) -> bool {
    crate::effects::effect_satisfies(effect, "Env.set")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::lexer::Lexer;
    use crate::parser::Parser;

    fn parse_items(src: &str) -> Vec<TopLevel> {
        let mut lexer = Lexer::new(src);
        let tokens = lexer.tokenize().expect("lex failed");
        let mut parser = Parser::new(tokens);
        parser.parse().expect("parse failed")
    }

    #[test]
    fn warns_on_console_effects_in_independent_product() {
        let items = parse_items(
            r#"
fn left() -> Unit
    ! [Console.print]
    Console.print("a")

fn right() -> Unit
    ! [Console.error]
    Console.error("b")

fn demo() -> (Unit, Unit)
    ! [Console.print, Console.error]
    (left(), right())!
"#,
        );
        let tc = crate::types::checker::run_type_check_full(&items, None);
        assert!(
            tc.errors.is_empty(),
            "unexpected type errors: {:?}",
            tc.errors
        );

        let warnings = collect_independence_warnings(&items, &tc.fn_sigs);
        assert_eq!(warnings.len(), 1, "warnings={warnings:?}");
        assert!(
            warnings[0]
                .message
                .contains("Independent product branches 1 and 2")
        );
        assert!(warnings[0].message.contains("Console.error"));
        assert!(warnings[0].message.contains("Console.print"));
        assert_eq!(warnings[0].fn_name.as_deref(), Some("demo"));
    }

    #[test]
    fn no_warning_for_independent_read_only_effects() {
        let items = parse_items(
            r#"
fn left() -> Bool
    ! [Disk.exists]
    Disk.exists("settings.json")

fn right() -> Option<String>
    ! [Env.get]
    Env.get("USER")

fn demo() -> (Bool, Option<String>)
    ! [Disk.exists, Env.get]
    (left(), right())!
"#,
        );
        let tc = crate::types::checker::run_type_check_full(&items, None);
        assert!(
            tc.errors.is_empty(),
            "unexpected type errors: {:?}",
            tc.errors
        );

        let warnings = collect_independence_warnings(&items, &tc.fn_sigs);
        assert!(
            warnings.is_empty(),
            "did not expect independence warning, got {warnings:?}"
        );
    }

    #[test]
    fn broad_namespace_effects_still_warn() {
        let items = parse_items(
            r#"
fn left() -> Unit
    ! [Console]
    Console.print("a")

fn right() -> Unit
    ! [Console.warn]
    Console.warn("b")

fn demo() -> (Unit, Unit)
    ! [Console]
    (left(), right())!
"#,
        );
        let tc = crate::types::checker::run_type_check_full(&items, None);
        assert!(
            tc.errors.is_empty(),
            "unexpected type errors: {:?}",
            tc.errors
        );

        let warnings = collect_independence_warnings(&items, &tc.fn_sigs);
        assert_eq!(warnings.len(), 1, "warnings={warnings:?}");
        assert!(warnings[0].message.contains("Console"));
    }

    #[test]
    fn warns_on_console_terminal_cross_namespace_hazard() {
        let items = parse_items(
            r#"
fn left() -> Unit
    ! [Console.print]
    Console.print("a")

fn right() -> Unit
    ! [Terminal.flush]
    Terminal.flush()

fn demo() -> (Unit, Unit)
    ! [Console.print, Terminal.flush]
    (left(), right())!
"#,
        );
        let tc = crate::types::checker::run_type_check_full(&items, None);
        assert!(
            tc.errors.is_empty(),
            "unexpected type errors: {:?}",
            tc.errors
        );

        let warnings = collect_independence_warnings(&items, &tc.fn_sigs);
        assert_eq!(warnings.len(), 1, "warnings={warnings:?}");
        assert!(warnings[0].message.contains("Console.print"));
        assert!(warnings[0].message.contains("Terminal.flush"));
        assert!(
            warnings[0]
                .message
                .contains("shared terminal/output hazard")
        );
    }
}
