// === crates/m1nd-ingest/src/extract/mod.rs ===

use m1nd_core::error::M1ndResult;
use m1nd_core::types::NodeType;

pub mod generic;
pub mod go;
pub mod java;
pub mod python;
pub mod rust_lang;
pub mod typescript;

#[cfg(feature = "tier1")]
pub mod tree_sitter_ext;

// ---------------------------------------------------------------------------
// Comment/string stripping — shared preprocessing for all extractors
// FM-ING-010: strip comments and string literals before regex extraction
// so that e.g. `"fn main()"` in a string literal is not extracted as a function.
// ---------------------------------------------------------------------------

/// Language-specific comment syntax for the pre-processor.
#[derive(Clone, Copy)]
pub struct CommentSyntax {
    /// Single-line comment prefix (e.g., "//", "#", "--").
    pub line_comment: &'static str,
    /// Block comment open (e.g., "/*"). Empty string means none.
    pub block_open: &'static str,
    /// Block comment close (e.g., "*/"). Empty string means none.
    pub block_close: &'static str,
    /// Triple-quote doc comment (e.g., `"""` for Python). Empty string means none.
    pub triple_quote: &'static str,
}

impl CommentSyntax {
    pub const RUST: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };
    pub const PYTHON: Self = Self {
        line_comment: "#",
        block_open: "",
        block_close: "",
        triple_quote: "\"\"\"",
    };
    pub const C_STYLE: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };
    pub const GO: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };
    pub const GENERIC: Self = Self {
        line_comment: "#",
        block_open: "",
        block_close: "",
        triple_quote: "",
    };

    // --- Tier 1 tree-sitter languages ---

    pub const CPP: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };

    pub const CSHARP: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };

    pub const RUBY: Self = Self {
        line_comment: "#",
        block_open: "=begin",
        block_close: "=end",
        triple_quote: "",
    };

    pub const PHP: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };

    pub const SWIFT: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };

    pub const KOTLIN: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };

    pub const SCALA: Self = Self {
        line_comment: "//",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };

    pub const BASH: Self = Self {
        line_comment: "#",
        block_open: "",
        block_close: "",
        triple_quote: "",
    };

    pub const LUA: Self = Self {
        line_comment: "--",
        block_open: "--[[",
        block_close: "]]",
        triple_quote: "",
    };

    pub const R: Self = Self {
        line_comment: "#",
        block_open: "",
        block_close: "",
        triple_quote: "",
    };

    pub const HTML: Self = Self {
        line_comment: "",
        block_open: "<!--",
        block_close: "-->",
        triple_quote: "",
    };

    pub const CSS: Self = Self {
        line_comment: "",
        block_open: "/*",
        block_close: "*/",
        triple_quote: "",
    };
}

/// Strips comments and string literals from source text, line by line.
/// Returns a Vec of cleaned lines (one per input line).
/// Block comment / triple-quote state is tracked across lines.
///
/// Import lines (e.g., `import "fmt"`, `from 'react'`, `use crate::foo`)
/// have their string content preserved so that module names are not lost.
/// Only comments are stripped on import lines.
pub fn strip_comments_and_strings(text: &str, syntax: CommentSyntax) -> Vec<String> {
    let mut result = Vec::new();
    let mut in_block_comment = false;
    let mut in_triple_quote = false;

    for line in text.lines() {
        // If we are NOT inside a block comment / triple-quote **and** the
        // line looks like an import statement, preserve string content —
        // only strip comments.
        let preserve_strings = !in_block_comment && !in_triple_quote && is_import_line(line);

        let cleaned = strip_line(
            line,
            &syntax,
            &mut in_block_comment,
            &mut in_triple_quote,
            preserve_strings,
        );
        result.push(cleaned);
    }
    result
}

/// Returns true if `line` looks like an import/use statement in any
/// supported language, meaning string literals on this line contain
/// module names that must not be stripped.
fn is_import_line(line: &str) -> bool {
    let trimmed = line.trim_start();
    // Go / TS / JS / Java / Python: `import ...`
    if trimmed.starts_with("import ") || trimmed.starts_with("import\t") {
        return true;
    }
    // Python: `from foo import bar`
    if trimmed.starts_with("from ") || trimmed.starts_with("from\t") {
        return true;
    }
    // Rust: `use crate::...` or `pub use ...`
    if trimmed.starts_with("use ") || trimmed.starts_with("use\t") {
        return true;
    }
    if trimmed.starts_with("pub use ") || trimmed.starts_with("pub use\t") {
        return true;
    }
    // Go grouped import: line inside `import ( ... )` block — these are
    // typically just `"package/path"`, detected by leading quote after
    // optional whitespace.
    if trimmed.starts_with('"') || trimmed.starts_with('\'') {
        // Could be inside an import block; preserve conservatively.
        // Callers that are NOT in an import block still benefit because
        // a bare string-only line has no function/class defs to confuse.
        return true;
    }
    false
}

/// Strip a single line, mutating block-comment/triple-quote tracking state.
/// When `preserve_strings` is true, string literal content is kept intact
/// (only comments are stripped). This is used for import lines where module
/// names live inside quotes.
fn strip_line(
    line: &str,
    syntax: &CommentSyntax,
    in_block_comment: &mut bool,
    in_triple_quote: &mut bool,
    preserve_strings: bool,
) -> String {
    let mut out = String::with_capacity(line.len());
    let chars: Vec<char> = line.chars().collect();
    let len = chars.len();
    let mut i = 0;

    while i < len {
        // --- Inside a block comment: scan for close ---
        if *in_block_comment {
            if !syntax.block_close.is_empty() {
                let close_chars: Vec<char> = syntax.block_close.chars().collect();
                if i + close_chars.len() <= len
                    && chars[i..i + close_chars.len()] == close_chars[..]
                {
                    *in_block_comment = false;
                    i += close_chars.len();
                    continue;
                }
            }
            i += 1;
            continue;
        }

        // --- Inside a triple-quote string: scan for closing triple-quote ---
        if *in_triple_quote {
            if !syntax.triple_quote.is_empty() {
                let tq_chars: Vec<char> = syntax.triple_quote.chars().collect();
                if i + tq_chars.len() <= len && chars[i..i + tq_chars.len()] == tq_chars[..] {
                    *in_triple_quote = false;
                    i += tq_chars.len();
                    continue;
                }
            }
            i += 1;
            continue;
        }

        // --- Check for triple-quote open ---
        if !syntax.triple_quote.is_empty() {
            let tq_chars: Vec<char> = syntax.triple_quote.chars().collect();
            if i + tq_chars.len() <= len && chars[i..i + tq_chars.len()] == tq_chars[..] {
                *in_triple_quote = true;
                i += tq_chars.len();
                continue;
            }
        }

        // --- Check for block comment open ---
        if !syntax.block_open.is_empty() {
            let bo_chars: Vec<char> = syntax.block_open.chars().collect();
            if i + bo_chars.len() <= len && chars[i..i + bo_chars.len()] == bo_chars[..] {
                *in_block_comment = true;
                i += bo_chars.len();
                continue;
            }
        }

        // --- Check for line comment ---
        if !syntax.line_comment.is_empty() {
            let lc_chars: Vec<char> = syntax.line_comment.chars().collect();
            if i + lc_chars.len() <= len && chars[i..i + lc_chars.len()] == lc_chars[..] {
                // Rest of line is comment; stop processing this line
                break;
            }
        }

        // --- Check for string literals: "..." or '...' ---
        if chars[i] == '"' || chars[i] == '\'' {
            let quote = chars[i];
            out.push(quote); // keep the quote delimiters
            i += 1;
            // Skip content until matching close quote (handle escapes)
            while i < len {
                if chars[i] == '\\' {
                    if preserve_strings {
                        out.push(chars[i]);
                        if i + 1 < len {
                            out.push(chars[i + 1]);
                        }
                    }
                    i += 2; // skip escaped char
                    continue;
                }
                if chars[i] == quote {
                    out.push(quote);
                    i += 1;
                    break;
                }
                if preserve_strings {
                    out.push(chars[i]);
                }
                // Otherwise: strip string content (original behavior)
                i += 1;
            }
            continue;
        }

        out.push(chars[i]);
        i += 1;
    }

    out
}

// ---------------------------------------------------------------------------
// ExtractedNode / ExtractedEdge — extraction output
// Replaces: ingest.py per-extractor output tuples
// ---------------------------------------------------------------------------

/// A node extracted from source code.
#[derive(Clone, Debug)]
pub struct ExtractedNode {
    /// Unique ID within the file (e.g., "file::src/main.rs::fn::main").
    pub id: String,
    /// Human-readable label.
    pub label: String,
    /// Node type (function, class, struct, etc.).
    pub node_type: NodeType,
    /// Tags (e.g., ["async", "public", "test"]).
    pub tags: Vec<String>,
    /// Line number in source file.
    pub line: u32,
    /// End line number.
    pub end_line: u32,
}

/// An edge extracted from source code.
#[derive(Clone, Debug)]
pub struct ExtractedEdge {
    /// Source node ID.
    pub source: String,
    /// Target node ID (may be unresolved reference).
    pub target: String,
    /// Relation type (e.g., "contains", "calls", "imports", "ref::").
    pub relation: String,
    /// Edge weight (default 1.0).
    pub weight: f32,
}

/// Result of extracting a single file.
#[derive(Clone, Debug)]
pub struct ExtractionResult {
    pub nodes: Vec<ExtractedNode>,
    pub edges: Vec<ExtractedEdge>,
    /// Unresolved references (target IDs that need resolution).
    pub unresolved_refs: Vec<String>,
}

/// Max source lines folded into a code symbol's excerpt (signature + a few body
/// lines) and the hard character budget on the joined result.
pub const EXCERPT_MAX_LINES: usize = 4;
pub const EXCERPT_MAX_CHARS: usize = 320;
/// Max preceding comment lines folded into a symbol's excerpt as doc-intent context.
pub const EXCERPT_MAX_DOC_LINES: usize = 6;

/// If `trimmed` is a comment line, return its prose payload (text after the
/// marker); else None. Handles `///` `//!` `//` (Rust/JS/Go/C), `#`
/// (Python/Ruby/shell), and `/**` `/*` `*/` `*` block-comment bodies. Used to
/// fold a symbol's preceding doc comment into its embedding text.
fn comment_prose(trimmed: &str) -> Option<&str> {
    for marker in ["///", "//!", "//", "/**", "/*", "*/", "*", "#"] {
        if let Some(rest) = trimmed.strip_prefix(marker) {
            return Some(rest.trim());
        }
    }
    None
}

/// Derive a short behavioral excerpt for each non-File symbol from its own source
/// span (signature + first body lines), so downstream embeddings capture what a
/// symbol DOES — not just its name (without this, code symbols embed label-only).
/// Returns `(node_id, excerpt)` pairs; nodes with no usable span are omitted.
/// Language-agnostic: it slices the file text by the node's `[line, end_line]`
/// range. Non-UTF-8 content and File nodes are skipped.
pub fn compute_excerpts(result: &ExtractionResult, content: &[u8]) -> Vec<(String, String)> {
    let text = match std::str::from_utf8(content) {
        Ok(t) => t,
        Err(_) => return Vec::new(),
    };
    let lines: Vec<&str> = text.lines().collect();
    let mut out = Vec::new();
    for node in &result.nodes {
        if node.node_type == NodeType::File || node.line == 0 {
            continue;
        }
        let start = (node.line as usize) - 1;
        if start >= lines.len() {
            continue;
        }
        // Cap by the symbol's real end ONLY when the extractor tracked it
        // (end_line > line). Regex-based extractors leave end_line == line, so
        // fall back to the line budget to still fold in the first body lines —
        // the behavioral signal — rather than the signature alone.
        let upper = if (node.end_line as usize) > (node.line as usize) {
            (node.end_line as usize).min(lines.len())
        } else {
            lines.len()
        };
        let stop = (start + EXCERPT_MAX_LINES)
            .min(upper)
            .max(start + 1)
            .min(lines.len());
        // Fold in the symbol's PRECEDING doc comment (rustdoc/JSDoc/Go/`#`) — the
        // purest statement of INTENT — scanning upward, skipping Rust attributes,
        // stopping at a blank line (so file-top license headers are excluded).
        let mut doc: Vec<&str> = Vec::new();
        let mut i = start;
        let mut scanned = 0;
        while i > 0 && scanned < EXCERPT_MAX_DOC_LINES {
            i -= 1;
            let t = lines[i].trim();
            if t.is_empty() {
                break;
            }
            if t.starts_with("#[") || t.starts_with("#![") {
                scanned += 1; // Rust attribute between doc and item — skip, keep scanning
                continue;
            }
            match comment_prose(t) {
                Some(prose) => {
                    if !prose.is_empty() {
                        doc.push(prose);
                    }
                    scanned += 1;
                }
                None => break, // reached real code
            }
        }
        doc.reverse(); // back into source order

        // excerpt = doc prose (intent) + signature/body, joined and char-bounded.
        let mut excerpt = String::new();
        let body = lines[start..stop].iter().map(|l| l.trim());
        for piece in doc.into_iter().chain(body) {
            if piece.is_empty() {
                continue;
            }
            if !excerpt.is_empty() {
                excerpt.push(' ');
            }
            excerpt.push_str(piece);
            if excerpt.chars().count() >= EXCERPT_MAX_CHARS {
                break;
            }
        }
        let excerpt: String = excerpt.chars().take(EXCERPT_MAX_CHARS).collect();
        if !excerpt.is_empty() {
            out.push((node.id.clone(), excerpt));
        }
    }
    out
}

// ---------------------------------------------------------------------------
// Extractor — trait for language-specific extraction
// Replaces: ingest.py PythonExtractor, TypeScriptExtractor, etc.
// ---------------------------------------------------------------------------

/// Language-specific code structure extractor.
/// All impls use tree-sitter (not regex) for correct extraction.
/// FM-ING-009: tree-sitter captures indented defs.
/// FM-ING-010: tree-sitter AST excludes strings/comments.
pub trait Extractor: Send + Sync {
    /// Extract nodes and edges from file content.
    /// `file_id` is the canonical file identifier (e.g., "file::src/main.rs").
    fn extract(&self, content: &[u8], file_id: &str) -> M1ndResult<ExtractionResult>;

    /// File extensions this extractor handles.
    fn extensions(&self) -> &[&str];
}

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

    #[test]
    fn compute_excerpts_slices_signature_and_body_and_skips_files() {
        let src = "/// Drains in-flight work on cancellation.\npub fn drain_inflight(ctx: &Ctx) -> Result<()> {\n    abort_running_work();\n    stop_accepting_tasks();\n}\n";
        let result = ExtractionResult {
            nodes: vec![
                ExtractedNode {
                    id: "file::x.rs::fn::drain_inflight".into(),
                    label: "drain_inflight".into(),
                    node_type: NodeType::Function,
                    tags: vec![],
                    line: 2, // the `pub fn ...` line (1-based)
                    end_line: 5,
                },
                ExtractedNode {
                    id: "file::x.rs".into(),
                    label: "x.rs".into(),
                    node_type: NodeType::File,
                    tags: vec![],
                    line: 1,
                    end_line: 5,
                },
            ],
            edges: vec![],
            unresolved_refs: vec![],
        };
        let ex = compute_excerpts(&result, src.as_bytes());
        // File node is skipped; the function node gets a behavioral excerpt.
        assert_eq!(ex.len(), 1, "only the non-file symbol gets an excerpt");
        let (id, text) = &ex[0];
        assert_eq!(id, "file::x.rs::fn::drain_inflight");
        assert!(
            text.contains("drain_inflight"),
            "excerpt carries the signature: {text}"
        );
        assert!(
            text.contains("abort_running_work"),
            "excerpt folds in the first body line(s): {text}"
        );
        assert!(
            text.contains("cancellation"),
            "excerpt folds in the preceding doc comment (intent): {text}"
        );
        assert!(text.chars().count() <= EXCERPT_MAX_CHARS);
    }
}
