use std::collections::{HashMap, HashSet};
use std::path::Path;

use streaming_iterator::StreamingIterator;
use tree_sitter::{Parser, Query, QueryCursor};

/// Result of scanning the app's own source for package imports
pub struct AppImports {
    /// Set of package names directly imported by the app
    pub packages: HashSet<String>,
    /// Map of package name → [(file, line)] showing where it's imported
    #[allow(dead_code)]
    pub locations: HashMap<String, Vec<(String, usize)>>,
}

/// Directories to scan for app source code
const SOURCE_DIRS: &[&str] = &["src", "app", "lib", "packages", "apps"];

/// File extensions to parse for imports (JS/TS)
const SOURCE_EXTENSIONS_JS: &[&str] = &["js", "mjs", "cjs", "ts", "mts", "cts", "jsx", "tsx"];
/// Python extensions
const SOURCE_EXTENSIONS_PY: &[&str] = &["py"];
/// Ruby extensions
const SOURCE_EXTENSIONS_RB: &[&str] = &["rb", "rake"];
/// Rust extensions
const SOURCE_EXTENSIONS_RS: &[&str] = &["rs"];

/// Scan the project's own source files to find which packages are imported.
/// This only scans YOUR code, not node_modules.
pub fn scan_app_imports(root: &Path) -> AppImports {
    let mut packages = HashSet::new();
    let mut locations: HashMap<String, Vec<(String, usize)>> = HashMap::new();

    let mut js_parser = Parser::new();
    js_parser
        .set_language(&tree_sitter_javascript::LANGUAGE.into())
        .expect("failed to set JS language");

    let mut ts_parser = Parser::new();
    ts_parser
        .set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
        .expect("failed to set TS language");

    let mut py_parser = crate::ast::python::new_parser();
    let mut rb_parser = crate::ast::ruby::new_parser();
    let mut rs_parser = crate::ast::rust_lang::new_parser();

    // Find all source files
    for source_dir in SOURCE_DIRS {
        let dir = root.join(source_dir);
        if !dir.exists() {
            continue;
        }

        for entry in walkdir::WalkDir::new(&dir)
            .into_iter()
            .filter_entry(|e| {
                let name = e.file_name().to_str().unwrap_or("");
                name != "node_modules"
                    && name != ".svelte-kit"
                    && name != "dist"
                    && name != "build"
                    && name != "__pycache__"
                    && name != ".venv"
                    && name != "venv"
                    && name != "target"
                    && name != "vendor"
            })
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().is_file())
        {
            let path = entry.path();
            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");

            let content = match std::fs::read_to_string(path) {
                Ok(c) => c,
                Err(_) => continue,
            };

            let rel_path = path
                .strip_prefix(root)
                .unwrap_or(path)
                .to_string_lossy()
                .to_string();

            if SOURCE_EXTENSIONS_JS.contains(&ext) {
                // Parse JS/TS with tree-sitter
                let parser = if ext == "ts" || ext == "mts" || ext == "cts" || ext == "tsx" {
                    &mut ts_parser
                } else {
                    &mut js_parser
                };

                extract_imports_ts(parser, &content, &rel_path, &mut packages, &mut locations);
            } else if ext == "svelte" || ext == "vue" {
                // Extract <script> block and parse as JS/TS
                if let Some(script_content) = extract_script_block(&content) {
                    let script_has_ts =
                        content.contains("lang=\"ts\"") || content.contains("lang='ts'");
                    let parser = if script_has_ts {
                        &mut ts_parser
                    } else {
                        &mut js_parser
                    };
                    extract_imports_ts(
                        parser,
                        &script_content,
                        &rel_path,
                        &mut packages,
                        &mut locations,
                    );
                }
            } else if SOURCE_EXTENSIONS_PY.contains(&ext) {
                let imports = crate::ast::python::extract_imports(&mut py_parser, &content);
                for module in imports {
                    packages.insert(module.clone());
                    locations
                        .entry(module)
                        .or_default()
                        .push((rel_path.clone(), 0));
                }
            } else if SOURCE_EXTENSIONS_RB.contains(&ext) {
                let requires = crate::ast::ruby::extract_requires(&mut rb_parser, &content);
                for module in requires {
                    packages.insert(module.clone());
                    locations
                        .entry(module)
                        .or_default()
                        .push((rel_path.clone(), 0));
                }
            } else if SOURCE_EXTENSIONS_RS.contains(&ext) {
                let uses = crate::ast::rust_lang::extract_uses(&mut rs_parser, &content);
                for crate_name in uses {
                    packages.insert(crate_name.clone());
                    locations
                        .entry(crate_name)
                        .or_default()
                        .push((rel_path.clone(), 0));
                }
            }
        }
    }

    // Also scan root-level config files (svelte.config.js, vite.config.ts, etc.)
    for config_file in &[
        "svelte.config.js",
        "vite.config.ts",
        "vite.config.js",
        "next.config.js",
        "next.config.ts",
        "tailwind.config.js",
        "tailwind.config.ts",
        "vitest.config.ts",
    ] {
        let path = root.join(config_file);
        if let Ok(content) = std::fs::read_to_string(&path) {
            let is_ts = config_file.ends_with(".ts");
            let parser = if is_ts {
                &mut ts_parser
            } else {
                &mut js_parser
            };
            // Config file imports are build-time, but we include them for completeness
            extract_imports_ts(parser, &content, config_file, &mut packages, &mut locations);
        }
    }

    AppImports {
        packages,
        locations,
    }
}

/// Extract package imports from JS/TS using tree-sitter
fn extract_imports_ts(
    parser: &mut Parser,
    content: &str,
    file_path: &str,
    packages: &mut HashSet<String>,
    locations: &mut HashMap<String, Vec<(String, usize)>>,
) {
    let tree = match parser.parse(content, None) {
        Some(t) => t,
        None => return,
    };

    let source = content.as_bytes();

    // Query for ES imports: import X from 'package'
    let import_query = Query::new(
        &tree.language(),
        r#"
        (import_statement
          source: (string (string_fragment) @module))
        "#,
    );

    if let Ok(query) = import_query {
        let module_idx = query
            .capture_names()
            .iter()
            .position(|n| *n == "module")
            .unwrap();

        let mut cursor = QueryCursor::new();
        let mut matches = cursor.matches(&query, tree.root_node(), source);

        while let Some(m) = matches.next() {
            if let Some(cap) = m.captures.iter().find(|c| c.index as usize == module_idx) {
                let module = cap.node.utf8_text(source).unwrap_or("");
                if let Some(pkg) = extract_package_from_module(module) {
                    let line = cap.node.start_position().row + 1;
                    packages.insert(pkg.clone());
                    locations
                        .entry(pkg)
                        .or_default()
                        .push((file_path.to_string(), line));
                }
            }
        }
    }

    // Query for CommonJS requires: const X = require('package')
    let require_query = Query::new(
        &tree.language(),
        r#"
        (call_expression
          function: (identifier) @fn
          arguments: (arguments (string (string_fragment) @module))
          (#eq? @fn "require"))
        "#,
    );

    if let Ok(query) = require_query {
        let module_idx = query
            .capture_names()
            .iter()
            .position(|n| *n == "module")
            .unwrap();

        let mut cursor = QueryCursor::new();
        let mut matches = cursor.matches(&query, tree.root_node(), source);

        while let Some(m) = matches.next() {
            if let Some(cap) = m.captures.iter().find(|c| c.index as usize == module_idx) {
                let module = cap.node.utf8_text(source).unwrap_or("");
                if let Some(pkg) = extract_package_from_module(module) {
                    let line = cap.node.start_position().row + 1;
                    packages.insert(pkg.clone());
                    locations
                        .entry(pkg)
                        .or_default()
                        .push((file_path.to_string(), line));
                }
            }
        }
    }

    // Query for dynamic imports: import('package')
    let dynamic_query = Query::new(
        &tree.language(),
        r#"
        (call_expression
          function: (import)
          arguments: (arguments (string (string_fragment) @module)))
        "#,
    );

    if let Ok(query) = dynamic_query {
        let module_idx = query
            .capture_names()
            .iter()
            .position(|n| *n == "module")
            .unwrap();

        let mut cursor = QueryCursor::new();
        let mut matches = cursor.matches(&query, tree.root_node(), source);

        while let Some(m) = matches.next() {
            if let Some(cap) = m.captures.iter().find(|c| c.index as usize == module_idx) {
                let module = cap.node.utf8_text(source).unwrap_or("");
                if let Some(pkg) = extract_package_from_module(module) {
                    let line = cap.node.start_position().row + 1;
                    packages.insert(pkg.clone());
                    locations
                        .entry(pkg)
                        .or_default()
                        .push((file_path.to_string(), line));
                }
            }
        }
    }
}

/// Convert a module specifier to a package name.
/// "lodash" → "lodash"
/// "@scope/pkg" → "@scope/pkg"
/// "@scope/pkg/sub/path" → "@scope/pkg"
/// "./relative" → None (local import)
/// "fs" → "fs" (builtin, but we include it)
fn extract_package_from_module(module: &str) -> Option<String> {
    if module.starts_with('.') || module.starts_with('/') {
        return None; // Relative or absolute path
    }
    if module.starts_with('$') {
        return None; // SvelteKit alias ($lib, $app, etc.)
    }

    let parts: Vec<&str> = module.split('/').collect();
    if parts[0].starts_with('@') && parts.len() >= 2 {
        Some(format!("{}/{}", parts[0], parts[1]))
    } else {
        Some(parts[0].to_string())
    }
}

/// Extract the content of <script> tags from Svelte/Vue files
fn extract_script_block(content: &str) -> Option<String> {
    let mut scripts = String::new();

    let mut remaining = content;
    while let Some(start) = remaining.find("<script") {
        let after_tag = &remaining[start..];
        if let Some(close_bracket) = after_tag.find('>') {
            let script_start = start + close_bracket + 1;
            if let Some(end) = remaining[script_start..].find("</script>") {
                scripts.push_str(&remaining[script_start..script_start + end]);
                scripts.push('\n');
                remaining = &remaining[script_start + end + 9..];
                continue;
            }
        }
        break;
    }

    if scripts.is_empty() {
        None
    } else {
        Some(scripts)
    }
}

/// Package category information for distinguishing dev vs prod dependencies.
pub struct PackageCategories {
    /// Packages in "dependencies" (production)
    #[allow(dead_code)]
    pub production: HashSet<String>,
    /// Packages in "devDependencies"
    pub dev: HashSet<String>,
}

/// Read dependency categories from package.json (and workspace package.jsons).
/// Returns production and dev dependency sets for reachability-based severity.
pub fn read_package_categories(root: &Path) -> PackageCategories {
    let mut production = HashSet::new();
    let mut dev = HashSet::new();

    // Read root package.json
    read_deps_from_package_json(&root.join("package.json"), &mut production, &mut dev);

    // Read workspace package.jsons (monorepo support)
    // Check for workspaces field in root package.json
    if let Ok(content) = std::fs::read_to_string(root.join("package.json")) {
        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content) {
            if let Some(workspaces) = parsed["workspaces"].as_array() {
                for ws in workspaces {
                    if let Some(pattern) = ws.as_str() {
                        // Expand simple glob patterns like "apps/*", "packages/*"
                        let base = pattern.trim_end_matches('*').trim_end_matches('/');
                        let ws_dir = root.join(base);
                        if ws_dir.is_dir() {
                            if let Ok(entries) = std::fs::read_dir(&ws_dir) {
                                for entry in entries.flatten() {
                                    let pkg_json = entry.path().join("package.json");
                                    read_deps_from_package_json(
                                        &pkg_json,
                                        &mut production,
                                        &mut dev,
                                    );
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // Also read Cargo.toml [dev-dependencies] if present
    let cargo_toml = root.join("Cargo.toml");
    if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
        if let Ok(parsed) = content.parse::<toml::Table>() {
            if let Some(deps) = parsed.get("dependencies").and_then(|d| d.as_table()) {
                for key in deps.keys() {
                    production.insert(key.clone());
                }
            }
            if let Some(deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
                for key in deps.keys() {
                    dev.insert(key.clone());
                }
            }
        }
    }

    PackageCategories { production, dev }
}

fn read_deps_from_package_json(
    path: &Path,
    production: &mut HashSet<String>,
    dev: &mut HashSet<String>,
) {
    if let Ok(content) = std::fs::read_to_string(path) {
        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content) {
            if let Some(deps) = parsed["dependencies"].as_object() {
                for key in deps.keys() {
                    production.insert(key.clone());
                }
            }
            if let Some(deps) = parsed["devDependencies"].as_object() {
                for key in deps.keys() {
                    dev.insert(key.clone());
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_extract_package_from_module() {
        assert_eq!(extract_package_from_module("lodash"), Some("lodash".into()));
        assert_eq!(
            extract_package_from_module("@scope/pkg"),
            Some("@scope/pkg".into())
        );
        assert_eq!(
            extract_package_from_module("@scope/pkg/sub/path"),
            Some("@scope/pkg".into())
        );
        assert_eq!(extract_package_from_module("./relative"), None);
        assert_eq!(extract_package_from_module("../parent"), None);
        assert_eq!(extract_package_from_module("$lib/utils"), None);
    }

    #[test]
    fn test_extract_script_block() {
        let svelte = r#"
<script lang="ts">
  import { onMount } from 'svelte';
  import { db } from '$lib/db';
</script>

<h1>Hello</h1>

<script context="module">
  import { load } from '@sveltejs/kit';
</script>
"#;
        let scripts = extract_script_block(svelte).unwrap();
        assert!(scripts.contains("import { onMount }"));
        assert!(scripts.contains("import { load }"));
    }

    #[test]
    fn test_extract_script_block_no_script() {
        let html = "<h1>Hello</h1>";
        assert!(extract_script_block(html).is_none());
    }

    #[test]
    fn test_extract_imports_from_js() {
        let mut parser = Parser::new();
        parser
            .set_language(&tree_sitter_javascript::LANGUAGE.into())
            .unwrap();

        let mut packages = HashSet::new();
        let mut locations = HashMap::new();

        let code = r#"
import { createClient } from '@supabase/supabase-js';
import lodash from 'lodash';
const fs = require('fs');
import('./dynamic-package');
import './relative-file';
"#;

        extract_imports_ts(&mut parser, code, "test.js", &mut packages, &mut locations);

        assert!(packages.contains("@supabase/supabase-js"));
        assert!(packages.contains("lodash"));
        assert!(packages.contains("fs"));
        assert!(!packages.contains("./relative-file"));
        // 3 confirmed: supabase, lodash, fs. Dynamic import may not be captured.
        assert!(packages.len() >= 3);
    }

    #[test]
    fn test_scan_app_imports_empty_dir() {
        let dir = tempfile::TempDir::new().unwrap();
        let imports = scan_app_imports(dir.path());
        assert!(imports.packages.is_empty());
    }

    #[test]
    fn test_scan_app_imports_python() {
        let dir = tempfile::TempDir::new().unwrap();
        let src = dir.path().join("src");
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(
            src.join("main.py"),
            "import requests\nfrom flask import Flask\n",
        )
        .unwrap();

        let imports = scan_app_imports(dir.path());
        assert!(imports.packages.contains("requests"));
        assert!(imports.packages.contains("flask"));
    }

    #[test]
    fn test_scan_app_imports_ruby() {
        let dir = tempfile::TempDir::new().unwrap();
        let src = dir.path().join("lib");
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(
            src.join("app.rb"),
            "require \"json\"\nrequire \"active_support/core_ext\"\n",
        )
        .unwrap();

        let imports = scan_app_imports(dir.path());
        assert!(imports.packages.contains("json"));
        assert!(imports.packages.contains("active_support"));
    }

    #[test]
    fn test_scan_app_imports_rust() {
        let dir = tempfile::TempDir::new().unwrap();
        let src = dir.path().join("src");
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(
            src.join("main.rs"),
            "use serde::Serialize;\nuse tokio::runtime;\n",
        )
        .unwrap();

        let imports = scan_app_imports(dir.path());
        assert!(imports.packages.contains("serde"));
        assert!(imports.packages.contains("tokio"));
    }
}
