//! Name resolution and qualified paths
//!
//! This module handles:
//! - Building a name table from parsed AST
//! - Mapping qualified paths to declarations
//! - Resolving `use` statements
//! - Fuzzy matching for suggestions

use indexmap::IndexMap;
use strsim::jaro_winkler;

use crate::{
    resolve::{
        ResolveError,
        Result,
    },
    syntax::ast::{
        Declaration,
        File,
        Span,
    },
};

/// A qualified path like `characters::Martha` or `behaviors::WorkAtBakery`
pub type QualifiedPath = Vec<String>;

/// Kind of declaration
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeclKind {
    Character,
    Template,
    LifeArc,
    Schedule,
    Behavior,
    Institution,
    Relationship,
    Location,
    Species,
}

/// Entry in the name table
#[derive(Debug, Clone)]
pub struct NameEntry {
    pub kind: DeclKind,
    pub qualified_path: QualifiedPath,
    pub span: Span,
    /// Index of the file this declaration came from (when building from
    /// multiple files)
    pub file_index: usize,
    /// Index into the file's declarations vector
    pub decl_index: usize,
}

/// Name table mapping qualified paths to declarations
#[derive(Debug, Clone)]
pub struct NameTable {
    /// Map from qualified path to entry
    entries: IndexMap<QualifiedPath, NameEntry>,
    /// Import statements (source path -> imported names)
    imports: Vec<Import>,
}

/// An import statement
#[derive(Debug, Clone)]
pub struct Import {
    pub source_path: QualifiedPath,
    pub kind: ImportKind,
}

#[derive(Debug, Clone)]
pub enum ImportKind {
    /// use foo::bar
    Single(String),
    /// use foo::{bar, baz}
    Grouped(Vec<String>),
    /// use foo::*
    Wildcard,
}

impl NameTable {
    /// Create a new empty name table
    pub fn new() -> Self {
        Self {
            entries: IndexMap::new(),
            imports: Vec::new(),
        }
    }

    /// Build a name table from a parsed file
    pub fn from_file(file: &File) -> Result<Self> {
        let mut table = Self::new();

        // First pass: collect imports
        for decl in &file.declarations {
            if let Declaration::Use(use_decl) = decl {
                let import = Import {
                    source_path: use_decl.path.clone(),
                    kind: match &use_decl.kind {
                        | crate::syntax::ast::UseKind::Single => {
                            // For single import, the last component is the name
                            let name = use_decl
                                .path
                                .last()
                                .ok_or_else(|| ResolveError::ValidationError {
                                    message: "Empty use path".to_string(),
                                    help: Some("Use statements must have at least one path component. For example: 'use characters::Martha;'".to_string()),
                                })?
                                .clone();
                            ImportKind::Single(name)
                        },
                        | crate::syntax::ast::UseKind::Grouped(names) => {
                            ImportKind::Grouped(names.clone())
                        },
                        | crate::syntax::ast::UseKind::Wildcard => ImportKind::Wildcard,
                    },
                };
                table.imports.push(import);
            }
        }

        // Second pass: register declarations
        for (index, decl) in file.declarations.iter().enumerate() {
            let (name, kind, span) = match decl {
                | Declaration::Use(_) => continue,
                | Declaration::Character(c) => {
                    (c.name.clone(), DeclKind::Character, c.span.clone())
                },
                | Declaration::Template(t) => (t.name.clone(), DeclKind::Template, t.span.clone()),
                | Declaration::LifeArc(l) => (l.name.clone(), DeclKind::LifeArc, l.span.clone()),
                | Declaration::Schedule(s) => (s.name.clone(), DeclKind::Schedule, s.span.clone()),
                | Declaration::Behavior(b) => (b.name.clone(), DeclKind::Behavior, b.span.clone()),
                | Declaration::Institution(i) => {
                    (i.name.clone(), DeclKind::Institution, i.span.clone())
                },
                | Declaration::Relationship(r) => {
                    (r.name.clone(), DeclKind::Relationship, r.span.clone())
                },
                | Declaration::Location(l) => (l.name.clone(), DeclKind::Location, l.span.clone()),
                | Declaration::Species(s) => (s.name.clone(), DeclKind::Species, s.span.clone()),
                | Declaration::Concept(_) |
                Declaration::SubConcept(_) |
                Declaration::Definition(_) => continue, /* TODO: Implement name resolution
                                                         * for type system */
            };

            // For now, qualified path is just the name
            // In a multi-file system, this would include directory structure
            let qualified_path = vec![name.clone()];

            // Check for duplicates
            if let Some(existing) = table.entries.get(&qualified_path) {
                return Err(ResolveError::DuplicateDefinition {
                    name,
                    first_location: (existing.span.start, existing.span.end),
                    second_location: (span.start, span.end),
                });
            }

            table.entries.insert(
                qualified_path.clone(),
                NameEntry {
                    kind,
                    qualified_path,
                    span,
                    file_index: 0, // Single file, so file_index is always 0
                    decl_index: index,
                },
            );
        }

        Ok(table)
    }

    /// Look up a name in the table
    pub fn lookup(&self, path: &[String]) -> Option<&NameEntry> {
        self.entries.get(path)
    }

    /// Find the best fuzzy match for a name
    pub fn find_suggestion(&self, name: &str) -> Option<String> {
        let mut best_match: Option<(String, f64)> = None;

        for path in self.entries.keys() {
            // Compare against the last component of the path
            let component = path.last()?;
            let score = jaro_winkler(name, component);

            if score > 0.8 {
                match &best_match {
                    | None => best_match = Some((component.clone(), score)),
                    | Some((_, best_score)) if score > *best_score => {
                        best_match = Some((component.clone(), score));
                    },
                    | _ => {},
                }
            }
        }

        best_match.map(|(name, _)| name)
    }

    /// Get all entries of a specific kind
    pub fn entries_of_kind(&self, kind: DeclKind) -> impl Iterator<Item = &NameEntry> {
        self.entries
            .values()
            .filter(move |entry| entry.kind == kind)
    }

    /// Get all entries
    pub fn all_entries(&self) -> impl Iterator<Item = &NameEntry> {
        self.entries.values()
    }

    /// Resolve a name, considering imports
    ///
    /// This tries to resolve a simple name like "Martha" by:
    /// 1. Looking in the local table
    /// 2. Checking if it's explicitly imported
    /// 3. Checking if it matches a wildcard import
    pub fn resolve_name(&self, name: &str) -> Option<&NameEntry> {
        // First try direct lookup (fully qualified or local)
        if let Some(entry) = self.lookup(&[name.to_string()]) {
            return Some(entry);
        }

        // Then try imports
        for import in &self.imports {
            match &import.kind {
                | ImportKind::Single(imported_name) if imported_name == name => {
                    // Found in single import
                    return self.lookup(&import.source_path);
                },
                | ImportKind::Grouped(names) if names.contains(&name.to_string()) => {
                    // Found in grouped import
                    let mut path = import.source_path.clone();
                    path.push(name.to_string());
                    return self.lookup(&path);
                },
                | ImportKind::Wildcard => {
                    // Try appending the name to the import path
                    let mut path = import.source_path.clone();
                    path.push(name.to_string());
                    if let Some(entry) = self.lookup(&path) {
                        return Some(entry);
                    }
                },
                | _ => {},
            }
        }

        None
    }

    /// Get all imports
    pub fn imports(&self) -> &[Import] {
        &self.imports
    }

    /// Merge another name table into this one
    ///
    /// This is used to combine name tables from multiple files into a single
    /// project-wide table. Returns an error if there are duplicate definitions.
    pub fn merge(&mut self, other: NameTable) -> Result<()> {
        // Merge imports
        self.imports.extend(other.imports);

        // Merge entries, checking for duplicates
        for (path, entry) in other.entries {
            if let Some(existing) = self.entries.get(&path) {
                // Found a duplicate - extract the name from the path
                let name = path.last().unwrap_or(&String::new()).clone();
                return Err(ResolveError::DuplicateDefinition {
                    name,
                    first_location: (existing.span.start, existing.span.end),
                    second_location: (entry.span.start, entry.span.end),
                });
            }
            self.entries.insert(path, entry);
        }

        Ok(())
    }

    /// Build a combined name table from multiple files
    ///
    /// This creates name tables for each file and merges them together,
    /// detecting any duplicate definitions across files.
    pub fn from_files(files: &[File]) -> Result<Self> {
        let mut combined = NameTable::new();

        for (file_index, file) in files.iter().enumerate() {
            let mut table = NameTable::from_file(file)?;
            // Update all entries to have the correct file_index
            for entry in table.entries.values_mut() {
                entry.file_index = file_index;
            }
            combined.merge(table)?;
        }

        Ok(combined)
    }
}

impl Default for NameTable {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::syntax::ast::{
        Character,
        Template,
    };

    #[test]
    fn test_name_table_basic() {
        let file = File {
            declarations: vec![
                Declaration::Character(Character {
                    name: "Martha".to_string(),
                    species: None,
                    fields: vec![],
                    template: None,
                    uses_behaviors: None,
                    uses_schedule: None,

                    span: Span::new(0, 10),
                }),
                Declaration::Template(Template {
                    name: "Person".to_string(),
                    species_base: None,
                    fields: vec![],
                    strict: false,
                    includes: vec![],
                    uses_behaviors: None,
                    uses_schedule: None,
                    span: Span::new(20, 30),
                }),
            ],
        };

        let table = NameTable::from_file(&file).unwrap();

        assert!(table.lookup(&["Martha".to_string()]).is_some());
        assert!(table.lookup(&["Person".to_string()]).is_some());
        assert!(table.lookup(&["Unknown".to_string()]).is_none());
    }

    #[test]
    fn test_duplicate_detection() {
        let file = File {
            declarations: vec![
                Declaration::Character(Character {
                    name: "Martha".to_string(),
                    species: None,
                    fields: vec![],
                    template: None,
                    uses_behaviors: None,
                    uses_schedule: None,

                    span: Span::new(0, 10),
                }),
                Declaration::Character(Character {
                    name: "Martha".to_string(),
                    species: None,
                    fields: vec![],
                    template: None,
                    uses_behaviors: None,
                    uses_schedule: None,

                    span: Span::new(20, 30),
                }),
            ],
        };

        let result = NameTable::from_file(&file);
        assert!(result.is_err());
    }

    #[test]
    fn test_fuzzy_matching() {
        let file = File {
            declarations: vec![Declaration::Character(Character {
                name: "Martha".to_string(),
                species: None,
                fields: vec![],
                template: None,
                uses_behaviors: None,
                uses_schedule: None,

                span: Span::new(0, 10),
            })],
        };

        let table = NameTable::from_file(&file).unwrap();

        // "Marhta" (typo) should suggest "Martha"
        let suggestion = table.find_suggestion("Marhta");
        assert_eq!(suggestion, Some("Martha".to_string()));
    }

    #[test]
    fn test_single_import() {
        use crate::syntax::ast::{
            UseDecl,
            UseKind,
        };

        let file = File {
            declarations: vec![
                Declaration::Use(UseDecl {
                    path: vec!["characters".to_string(), "Martha".to_string()],
                    kind: UseKind::Single,
                    span: Span::new(0, 10),
                }),
                Declaration::Character(Character {
                    name: "characters".to_string(),
                    species: None,
                    fields: vec![],
                    template: None,
                    uses_behaviors: None,
                    uses_schedule: None,

                    span: Span::new(20, 30),
                }),
            ],
        };

        let table = NameTable::from_file(&file).unwrap();
        assert_eq!(table.imports.len(), 1);

        match &table.imports[0].kind {
            | ImportKind::Single(name) => assert_eq!(name, "Martha"),
            | _ => panic!("Expected single import"),
        }
    }

    #[test]
    fn test_grouped_import() {
        use crate::syntax::ast::{
            UseDecl,
            UseKind,
        };

        let file = File {
            declarations: vec![Declaration::Use(UseDecl {
                path: vec!["characters".to_string()],
                kind: UseKind::Grouped(vec!["Martha".to_string(), "David".to_string()]),
                span: Span::new(0, 10),
            })],
        };

        let table = NameTable::from_file(&file).unwrap();
        assert_eq!(table.imports.len(), 1);

        match &table.imports[0].kind {
            | ImportKind::Grouped(names) => {
                assert_eq!(names.len(), 2);
                assert!(names.contains(&"Martha".to_string()));
                assert!(names.contains(&"David".to_string()));
            },
            | _ => panic!("Expected grouped import"),
        }
    }

    #[test]
    fn test_wildcard_import() {
        use crate::syntax::ast::{
            UseDecl,
            UseKind,
        };

        let file = File {
            declarations: vec![Declaration::Use(UseDecl {
                path: vec!["characters".to_string()],
                kind: UseKind::Wildcard,
                span: Span::new(0, 10),
            })],
        };

        let table = NameTable::from_file(&file).unwrap();
        assert_eq!(table.imports.len(), 1);

        match &table.imports[0].kind {
            | ImportKind::Wildcard => {},
            | _ => panic!("Expected wildcard import"),
        }
    }

    #[test]
    fn test_merge_tables() {
        let file1 = File {
            declarations: vec![Declaration::Character(Character {
                name: "Martha".to_string(),
                species: None,
                fields: vec![],
                template: None,
                uses_behaviors: None,
                uses_schedule: None,

                span: Span::new(0, 10),
            })],
        };

        let file2 = File {
            declarations: vec![Declaration::Character(Character {
                name: "David".to_string(),
                species: None,
                fields: vec![],
                template: None,
                uses_behaviors: None,
                uses_schedule: None,

                span: Span::new(0, 10),
            })],
        };

        let mut table1 = NameTable::from_file(&file1).unwrap();
        let table2 = NameTable::from_file(&file2).unwrap();

        table1.merge(table2).unwrap();

        // Both names should be in the merged table
        assert!(table1.lookup(&["Martha".to_string()]).is_some());
        assert!(table1.lookup(&["David".to_string()]).is_some());
    }

    #[test]
    fn test_merge_detects_duplicates() {
        let file1 = File {
            declarations: vec![Declaration::Character(Character {
                name: "Martha".to_string(),
                species: None,
                fields: vec![],
                template: None,
                uses_behaviors: None,
                uses_schedule: None,

                span: Span::new(0, 10),
            })],
        };

        let file2 = File {
            declarations: vec![Declaration::Character(Character {
                name: "Martha".to_string(),
                species: None,
                fields: vec![],
                template: None,
                uses_behaviors: None,
                uses_schedule: None,

                span: Span::new(20, 30),
            })],
        };

        let mut table1 = NameTable::from_file(&file1).unwrap();
        let table2 = NameTable::from_file(&file2).unwrap();

        let result = table1.merge(table2);
        assert!(result.is_err());
    }

    #[test]
    fn test_from_files() {
        let files = vec![
            File {
                declarations: vec![Declaration::Character(Character {
                    name: "Martha".to_string(),
                    species: None,
                    fields: vec![],
                    template: None,
                    uses_behaviors: None,
                    uses_schedule: None,

                    span: Span::new(0, 10),
                })],
            },
            File {
                declarations: vec![Declaration::Character(Character {
                    name: "David".to_string(),
                    species: None,
                    fields: vec![],
                    template: None,
                    uses_behaviors: None,
                    uses_schedule: None,

                    span: Span::new(0, 10),
                })],
            },
        ];

        let table = NameTable::from_files(&files).unwrap();

        assert!(table.lookup(&["Martha".to_string()]).is_some());
        assert!(table.lookup(&["David".to_string()]).is_some());
    }

    #[test]
    fn test_from_files_detects_duplicates() {
        let files = vec![
            File {
                declarations: vec![Declaration::Character(Character {
                    name: "Martha".to_string(),
                    species: None,
                    fields: vec![],
                    template: None,
                    uses_behaviors: None,
                    uses_schedule: None,

                    span: Span::new(0, 10),
                })],
            },
            File {
                declarations: vec![Declaration::Character(Character {
                    name: "Martha".to_string(),
                    species: None,
                    fields: vec![],
                    template: None,
                    uses_behaviors: None,
                    uses_schedule: None,

                    span: Span::new(20, 30),
                })],
            },
        ];

        let result = NameTable::from_files(&files);
        assert!(result.is_err());
    }
}
