use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use ucm_core::Document;

pub const CODEGRAPH_PROFILE: &str = "codegraph";
pub const CODEGRAPH_PROFILE_VERSION: &str = "v1";
pub const CODEGRAPH_PROFILE_MARKER: &str = "codegraph.v1";
pub const CODEGRAPH_EXTRACTOR_VERSION: &str = "ucp-codegraph-extractor.v1";

pub(crate) const META_NODE_CLASS: &str = "node_class";
pub(crate) const META_LOGICAL_KEY: &str = "logical_key";
pub(crate) const META_CODEREF: &str = "coderef";
pub(crate) const META_LANGUAGE: &str = "language";
pub(crate) const META_SYMBOL_KIND: &str = "symbol_kind";
pub(crate) const META_SYMBOL_NAME: &str = "name";
pub(crate) const META_EXPORTED: &str = "exported";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeGraphSeverity {
    Error,
    Warning,
    Info,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeGraphDiagnostic {
    pub severity: CodeGraphSeverity,
    pub code: String,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub logical_key: Option<String>,
}

impl CodeGraphDiagnostic {
    pub(crate) fn error(code: &str, message: impl Into<String>) -> Self {
        Self {
            severity: CodeGraphSeverity::Error,
            code: code.to_string(),
            message: message.into(),
            path: None,
            logical_key: None,
        }
    }

    pub(crate) fn warning(code: &str, message: impl Into<String>) -> Self {
        Self {
            severity: CodeGraphSeverity::Warning,
            code: code.to_string(),
            message: message.into(),
            path: None,
            logical_key: None,
        }
    }

    pub(crate) fn info(code: &str, message: impl Into<String>) -> Self {
        Self {
            severity: CodeGraphSeverity::Info,
            code: code.to_string(),
            message: message.into(),
            path: None,
            logical_key: None,
        }
    }

    pub(crate) fn with_path(mut self, path: impl Into<String>) -> Self {
        self.path = Some(path.into());
        self
    }

    pub(crate) fn with_logical_key(mut self, logical_key: impl Into<String>) -> Self {
        self.logical_key = Some(logical_key.into());
        self
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CodeGraphValidationResult {
    pub valid: bool,
    pub diagnostics: Vec<CodeGraphDiagnostic>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CodeGraphStats {
    pub total_nodes: usize,
    pub repository_nodes: usize,
    pub directory_nodes: usize,
    pub file_nodes: usize,
    pub symbol_nodes: usize,
    pub total_edges: usize,
    pub reference_edges: usize,
    pub export_edges: usize,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub languages: BTreeMap<String, usize>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeGraphBuildStatus {
    Success,
    PartialSuccess,
    FailedValidation,
}

#[derive(Debug, Clone)]
pub struct CodeGraphBuildResult {
    pub document: Document,
    pub diagnostics: Vec<CodeGraphDiagnostic>,
    pub stats: CodeGraphStats,
    pub profile_version: String,
    pub canonical_fingerprint: String,
    pub status: CodeGraphBuildStatus,
    pub incremental: Option<CodeGraphIncrementalStats>,
}

impl CodeGraphBuildResult {
    pub fn has_errors(&self) -> bool {
        self.diagnostics
            .iter()
            .any(|d| d.severity == CodeGraphSeverity::Error)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeGraphBuildInput {
    pub repository_path: PathBuf,
    pub commit_hash: String,
    #[serde(default)]
    pub config: CodeGraphExtractorConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeGraphIncrementalBuildInput {
    pub build: CodeGraphBuildInput,
    pub state_file: PathBuf,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CodeGraphIncrementalStats {
    #[serde(default)]
    pub requested: bool,
    #[serde(default)]
    pub scanned_files: usize,
    #[serde(default)]
    pub state_entries: usize,
    #[serde(default)]
    pub direct_invalidated_files: usize,
    #[serde(default)]
    pub surface_changed_files: usize,
    #[serde(default)]
    pub reused_files: usize,
    #[serde(default)]
    pub rebuilt_files: usize,
    #[serde(default)]
    pub added_files: usize,
    #[serde(default)]
    pub changed_files: usize,
    #[serde(default)]
    pub deleted_files: usize,
    #[serde(default)]
    pub invalidated_files: usize,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub full_rebuild_reason: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeGraphExtractorConfig {
    #[serde(default = "default_include_extensions")]
    pub include_extensions: Vec<String>,
    #[serde(default = "default_exclude_dirs")]
    pub exclude_dirs: Vec<String>,
    #[serde(default = "default_continue_on_parse_error")]
    pub continue_on_parse_error: bool,
    #[serde(default)]
    pub include_hidden: bool,
    #[serde(default = "default_max_file_bytes")]
    pub max_file_bytes: usize,
    #[serde(default = "default_emit_export_edges")]
    pub emit_export_edges: bool,
}

impl Default for CodeGraphExtractorConfig {
    fn default() -> Self {
        Self {
            include_extensions: default_include_extensions(),
            exclude_dirs: default_exclude_dirs(),
            continue_on_parse_error: default_continue_on_parse_error(),
            include_hidden: false,
            max_file_bytes: default_max_file_bytes(),
            emit_export_edges: default_emit_export_edges(),
        }
    }
}

fn default_include_extensions() -> Vec<String> {
    vec!["rs", "py", "ts", "tsx", "js", "jsx"]
        .into_iter()
        .map(|s| s.to_string())
        .collect()
}

fn default_exclude_dirs() -> Vec<String> {
    vec![".git", "target", "node_modules", "dist", "build"]
        .into_iter()
        .map(|s| s.to_string())
        .collect()
}

fn default_continue_on_parse_error() -> bool {
    true
}

fn default_max_file_bytes() -> usize {
    2 * 1024 * 1024
}

fn default_emit_export_edges() -> bool {
    true
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CodeLanguage {
    Rust,
    Python,
    TypeScript,
    JavaScript,
}

impl CodeLanguage {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Rust => "rust",
            Self::Python => "python",
            Self::TypeScript => "typescript",
            Self::JavaScript => "javascript",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct RepoFile {
    pub(crate) absolute_path: PathBuf,
    pub(crate) relative_path: String,
    pub(crate) language: CodeLanguage,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExtractedSymbol {
    pub(crate) name: String,
    pub(crate) qualified_name: String,
    pub(crate) identity: String,
    pub(crate) parent_identity: Option<String>,
    pub(crate) kind: String,
    pub(crate) description: Option<String>,
    pub(crate) modifiers: ExtractedModifiers,
    pub(crate) inputs: Vec<ExtractedInput>,
    pub(crate) output: Option<String>,
    pub(crate) type_info: Option<String>,
    pub(crate) exported: bool,
    pub(crate) start_line: usize,
    pub(crate) start_col: usize,
    pub(crate) end_line: usize,
    pub(crate) end_col: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExtractedInput {
    pub(crate) name: String,
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub(crate) type_name: Option<String>,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct ExtractedSignature {
    pub(crate) inputs: Vec<ExtractedInput>,
    pub(crate) output: Option<String>,
    pub(crate) type_info: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub(crate) struct ExtractedModifiers {
    #[serde(rename = "async", skip_serializing_if = "is_false")]
    pub(crate) is_async: bool,
    #[serde(skip_serializing_if = "is_false")]
    pub(crate) generator: bool,
    #[serde(rename = "static", skip_serializing_if = "is_false")]
    pub(crate) is_static: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) visibility: Option<String>,
}

impl ExtractedModifiers {
    pub(crate) fn is_empty(&self) -> bool {
        !self.is_async && !self.generator && !self.is_static && self.visibility.is_none()
    }
}

fn is_false(value: &bool) -> bool {
    !*value
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExtractedImport {
    pub(crate) module: String,
    pub(crate) symbols: Vec<String>,
    pub(crate) bindings: Vec<ImportBinding>,
    pub(crate) module_aliases: Vec<String>,
    pub(crate) reexported: bool,
    pub(crate) wildcard: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub(crate) struct ImportBinding {
    pub(crate) source_name: String,
    pub(crate) local_name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExtractedRelationship {
    pub(crate) source_identity: String,
    pub(crate) relation: String,
    pub(crate) target_expr: String,
    pub(crate) target_name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExtractedUsage {
    pub(crate) source_identity: String,
    pub(crate) target_expr: String,
    pub(crate) target_name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExtractedAlias {
    pub(crate) name: String,
    pub(crate) target_expr: String,
    pub(crate) target_name: String,
    pub(crate) owner_identity: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ImportResolution {
    Resolved(String),
    External,
    Unresolved,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct FileAnalysis {
    pub(crate) file_description: Option<String>,
    pub(crate) symbols: Vec<ExtractedSymbol>,
    pub(crate) imports: Vec<ExtractedImport>,
    pub(crate) relationships: Vec<ExtractedRelationship>,
    pub(crate) usages: Vec<ExtractedUsage>,
    pub(crate) aliases: Vec<ExtractedAlias>,
    pub(crate) export_bindings: Vec<ImportBinding>,
    pub(crate) exported_symbol_names: BTreeSet<String>,
    pub(crate) default_exported_symbol_names: BTreeSet<String>,
    pub(crate) diagnostics: Vec<CodeGraphDiagnostic>,
}

#[derive(Debug, Clone)]
pub(crate) struct FileAnalysisRecord {
    pub(crate) file: String,
    pub(crate) language: CodeLanguage,
    pub(crate) imports: Vec<ExtractedImport>,
    pub(crate) relationships: Vec<ExtractedRelationship>,
    pub(crate) usages: Vec<ExtractedUsage>,
    pub(crate) aliases: Vec<ExtractedAlias>,
    pub(crate) export_bindings: Vec<ImportBinding>,
}

impl ExtractedImport {
    pub(crate) fn module(module: impl Into<String>) -> Self {
        Self {
            module: module.into(),
            symbols: Vec::new(),
            bindings: Vec::new(),
            module_aliases: Vec::new(),
            reexported: false,
            wildcard: false,
        }
    }

    pub(crate) fn symbol(module: impl Into<String>, symbol: impl Into<String>) -> Self {
        let symbol = symbol.into();
        Self {
            module: module.into(),
            symbols: vec![symbol.clone()],
            bindings: vec![ImportBinding::same(symbol)],
            module_aliases: Vec::new(),
            reexported: false,
            wildcard: false,
        }
    }

    pub(crate) fn bindings(module: impl Into<String>, bindings: Vec<ImportBinding>) -> Self {
        let mut symbols = bindings
            .iter()
            .map(|binding| binding.source_name.clone())
            .collect::<Vec<_>>();
        symbols.sort();
        symbols.dedup();

        Self {
            module: module.into(),
            symbols,
            bindings,
            module_aliases: Vec::new(),
            reexported: false,
            wildcard: false,
        }
    }

    pub(crate) fn with_module_alias(mut self, alias: impl Into<String>) -> Self {
        let alias = alias.into();
        if !alias.is_empty() && !self.module_aliases.contains(&alias) {
            self.module_aliases.push(alias);
        }
        self
    }

    pub(crate) fn reexported(mut self) -> Self {
        self.reexported = true;
        self
    }

    pub(crate) fn wildcard(mut self) -> Self {
        self.wildcard = true;
        self
    }
}

impl ImportBinding {
    pub(crate) fn new(source_name: impl Into<String>, local_name: impl Into<String>) -> Self {
        Self {
            source_name: source_name.into(),
            local_name: local_name.into(),
        }
    }

    pub(crate) fn same(name: impl Into<String>) -> Self {
        let name = name.into();
        Self::new(name.clone(), name)
    }
}

impl ExtractedRelationship {
    pub(crate) fn new(
        source_identity: impl Into<String>,
        relation: impl Into<String>,
        target_expr: impl Into<String>,
        target_name: impl Into<String>,
    ) -> Self {
        Self {
            source_identity: source_identity.into(),
            relation: relation.into(),
            target_expr: target_expr.into(),
            target_name: target_name.into(),
        }
    }
}

impl ExtractedUsage {
    pub(crate) fn new(
        source_identity: impl Into<String>,
        target_expr: impl Into<String>,
        target_name: impl Into<String>,
    ) -> Self {
        Self {
            source_identity: source_identity.into(),
            target_expr: target_expr.into(),
            target_name: target_name.into(),
        }
    }
}

impl ExtractedAlias {
    pub(crate) fn new(
        name: impl Into<String>,
        target_expr: impl Into<String>,
        target_name: impl Into<String>,
        owner_identity: Option<&str>,
    ) -> Self {
        Self {
            name: name.into(),
            target_expr: target_expr.into(),
            target_name: target_name.into(),
            owner_identity: owner_identity.map(str::to_string),
        }
    }
}
