#![deny(unused_variables)]
#![deny(dead_code)]

//! Storybook - A DSL for authoring narrative content for agent simulations
//!
//! This library provides parsing, resolution, and validation for `.sb` files.
//!
//! # Example
//!
//! ```no_run
//! use storybook::{
//!     query::CharacterQuery,
//!     Project,
//! };
//!
//! // Load and validate a storybook project
//! let project = Project::load("path/to/storybook")?;
//!
//! // Query for characters
//! for character in project.characters() {
//!     println!("Character: {}", character.name);
//! }
//!
//! // Filter characters by age and traits
//! for character in project
//!     .characters()
//!     .with_age_range(25, 40)
//!     .with_trait("trust", 0.7, 1.0)
//! {
//!     println!("Trusted character: {}", character.name);
//! }
//!
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```

// Suppress false positive warnings from thiserror macro
#![allow(unused_assignments)]

pub mod lsp;
pub mod position;
pub mod query;
pub mod resolve;
pub mod syntax;
pub mod types;

#[cfg(test)]
mod error_showcase_tests;

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

use miette::Diagnostic;
pub use resolve::{
    NameTable,
    QualifiedPath,
};
use thiserror::Error;
pub use types::*;

use crate::{
    resolve::validate,
    syntax::{
        ast::File,
        FileParser,
    },
};

/// Errors that can occur when working with projects
#[derive(Error, Debug, Diagnostic)]
pub enum ProjectError {
    #[error("Failed to parse file: {path}")]
    #[diagnostic(help("There's a syntax error in this file. Check the error details above for the specific issue. Common problems: missing braces, unclosed strings, incorrect syntax for declarations."))]
    #[allow(dead_code)]
    ParseError {
        path: String,
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },

    #[error(transparent)]
    #[diagnostic(transparent)]
    ResolveError(#[from] resolve::ResolveError),

    #[error("IO error reading files: {0}")]
    #[diagnostic(help("There was a problem reading or writing files. Check that: the file exists, you have permission to read it, the path is correct, and there's enough disk space."))]
    IoError(#[from] std::io::Error),

    #[error("Invalid project structure: {0}")]
    #[diagnostic(help("The project directory structure is not valid. Make sure: the path exists and is a directory, it contains at least one .sb file, you have permission to access it."))]
    InvalidStructure(String),
}

/// Result type for project operations
pub type Result<T> = std::result::Result<T, ProjectError>;

/// A loaded and validated Storybook project
#[derive(Debug, Clone)]
pub struct Project {
    /// Root path of the project (either a file or directory)
    pub root: PathBuf,
    /// All resolved files in the project
    pub files: Vec<ResolvedFile>,
    /// Combined name table across all files
    pub name_table: NameTable,
}

impl Project {
    /// Load a storybook project from a file or directory
    ///
    /// This will:
    /// 1. Find all `.sb` files (or load the single file if path is a file)
    /// 2. Parse each file
    /// 3. Build a combined name table
    /// 4. Resolve all cross-references
    /// 5. Validate semantic constraints
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The path doesn't exist
    /// - Any `.sb` file fails to parse
    /// - Name resolution fails (undefined references, duplicates, etc.)
    /// - Semantic validation fails (invalid ranges, overlaps, etc.)
    pub fn load<P: AsRef<Path>>(root: P) -> Result<Self> {
        let root = root.as_ref().to_path_buf();

        if !root.exists() {
            return Err(ProjectError::InvalidStructure(format!(
                "Path does not exist: {}",
                root.display()
            )));
        }

        // Handle both single files and directories
        let sb_files = if root.is_file() {
            // Single file - validate it's a .sb file
            if root.extension().and_then(|s| s.to_str()) != Some("sb") {
                return Err(ProjectError::InvalidStructure(format!(
                    "File must have .sb extension: {}",
                    root.display()
                )));
            }
            vec![root.clone()]
        } else if root.is_dir() {
            // Directory - find all .sb files
            Self::find_sb_files(&root)?
        } else {
            return Err(ProjectError::InvalidStructure(format!(
                "Path is neither a file nor a directory: {}",
                root.display()
            )));
        };

        if sb_files.is_empty() {
            return Err(ProjectError::InvalidStructure(
                "No .sb files found in project".to_string(),
            ));
        }

        // Parse all files
        let mut parsed_files = Vec::new();
        for path in &sb_files {
            let file = Self::parse_file(path)?;
            parsed_files.push(file);
        }

        // Build combined name table from all files
        let name_table = NameTable::from_files(&parsed_files)?;

        // Build action registry from schema if it exists
        let action_registry = Self::build_action_registry(&root)?;

        // Validate and convert all files
        let mut resolved_files = Vec::new();

        for file in &parsed_files {
            // First validate
            validate::validate_file(file, &action_registry)?;

            // Then convert AST to resolved types using the global name table and all parsed
            // files
            let declarations =
                resolve::convert::convert_file_with_all_files(file, &name_table, &parsed_files)?;

            resolved_files.push(ResolvedFile { declarations });
        }

        Ok(Project {
            root,
            files: resolved_files,
            name_table,
        })
    }

    /// Parse a single .sb file
    fn parse_file(path: &Path) -> Result<File> {
        let content = std::fs::read_to_string(path)?;
        let lexer = syntax::lexer::Lexer::new(&content);

        FileParser::new()
            .parse(lexer)
            .map_err(|e| ProjectError::ParseError {
                path: path.display().to_string(),
                source: Box::new(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    format!("Parse error: {:?}", e),
                )),
            })
    }

    /// Find all .sb files in a directory recursively
    fn find_sb_files(root: &Path) -> Result<Vec<PathBuf>> {
        let mut files = Vec::new();

        for entry in walkdir::WalkDir::new(root)
            .follow_links(true)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            let path = entry.path();
            if path.extension().and_then(|s| s.to_str()) == Some("sb") {
                files.push(path.to_path_buf());
            }
        }

        Ok(files)
    }

    /// Build action registry from schema files if they exist
    ///
    /// Looks for `schema/actions.sb` and extracts action names from `enum
    /// Action` declaration. If no schema exists, returns empty registry (no
    /// validation).
    ///
    /// # Future Tooling
    ///
    /// TODO: Add commands to help maintain the Action enum:
    /// - `sb actions check` - find actions used in behavior trees but not in
    ///   enum
    /// - `sb actions suggest` - auto-generate/update enum Action from usage
    /// - `sb actions unused` - find enum variants never used in behavior trees
    fn build_action_registry(root: &Path) -> Result<HashSet<String>> {
        let schema_path = if root.is_file() {
            // If root is a file, look for schema in parent directory
            root.parent().map(|p| p.join("schema").join("actions.sb"))
        } else {
            // If root is a directory, look for schema subdirectory
            Some(root.join("schema").join("actions.sb"))
        };

        let schema_path = match schema_path {
            | Some(path) if path.exists() => path,
            | _ => return Ok(HashSet::new()), // No schema, return empty registry
        };

        // Parse the schema file
        let _file = Self::parse_file(&schema_path)?;

        // TODO: Update to use new type system (concept/sub_concept)
        // For now, return empty registry as enum support is removed
        let registry = HashSet::new();

        Ok(registry)
    }

    /// Get all characters across all files
    pub fn characters(&self) -> impl Iterator<Item = &ResolvedCharacter> {
        self.files.iter().flat_map(|f| f.characters())
    }

    /// Get all relationships across all files
    pub fn relationships(&self) -> impl Iterator<Item = &ResolvedRelationship> {
        self.files.iter().flat_map(|f| f.relationships())
    }

    /// Get all institutions across all files
    pub fn institutions(&self) -> impl Iterator<Item = &ResolvedInstitution> {
        self.files.iter().flat_map(|f| f.institutions())
    }

    /// Get all schedules across all files
    pub fn schedules(&self) -> impl Iterator<Item = &ResolvedSchedule> {
        self.files.iter().flat_map(|f| f.schedules())
    }

    /// Get all behavior trees across all files
    pub fn behaviors(&self) -> impl Iterator<Item = &ResolvedBehavior> {
        self.files.iter().flat_map(|f| f.behaviors())
    }

    /// Get all life arcs across all files
    pub fn life_arcs(&self) -> impl Iterator<Item = &ResolvedLifeArc> {
        self.files.iter().flat_map(|f| f.life_arcs())
    }

    /// Get all locations across all files
    pub fn locations(&self) -> impl Iterator<Item = &ResolvedLocation> {
        self.files.iter().flat_map(|f| f.locations())
    }

    /// Get all species across all files
    pub fn species(&self) -> impl Iterator<Item = &ResolvedSpecies> {
        self.files.iter().flat_map(|f| f.species())
    }

    /// Find a character by name
    pub fn find_character(&self, name: &str) -> Option<&ResolvedCharacter> {
        self.characters().find(|c| c.name == name)
    }

    /// Find a relationship by name
    pub fn find_relationship(&self, name: &str) -> Option<&ResolvedRelationship> {
        self.relationships().find(|r| r.name == name)
    }

    /// Find an institution by name
    pub fn find_institution(&self, name: &str) -> Option<&ResolvedInstitution> {
        self.institutions().find(|i| i.name == name)
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn test_build_action_registry_no_schema() {
        let dir = TempDir::new().unwrap();

        let registry = Project::build_action_registry(dir.path()).unwrap();

        assert!(
            registry.is_empty(),
            "Registry should be empty when no schema exists"
        );
    }

    #[test]
    fn test_build_action_registry_with_schema() {
        let dir = TempDir::new().unwrap();
        let schema_dir = dir.path().join("schema");
        fs::create_dir(&schema_dir).unwrap();

        fs::write(
            schema_dir.join("actions.sb"),
            "// TODO: Update to use new type system (concept/sub_concept)\n",
        )
        .unwrap();

        let registry = Project::build_action_registry(dir.path()).unwrap();

        // Action registry is currently disabled during enum removal
        // TODO: Re-implement using concept/sub_concept system
        assert_eq!(registry.len(), 0);
    }

    #[test]
    fn test_build_action_registry_from_file_path() {
        let dir = TempDir::new().unwrap();
        let schema_dir = dir.path().join("schema");
        fs::create_dir(&schema_dir).unwrap();

        fs::write(
            schema_dir.join("actions.sb"),
            "// TODO: Update to use new type system",
        )
        .unwrap();

        // Create a test .sb file in the directory
        let test_file = dir.path().join("test.sb");
        fs::write(&test_file, "character Test { age: 30 }").unwrap();

        // Pass the file path - should look for schema in parent directory
        let registry = Project::build_action_registry(&test_file).unwrap();

        // Action registry is currently disabled during enum removal
        // TODO: Re-implement using concept/sub_concept system
        assert_eq!(registry.len(), 0);
    }

    #[test]
    fn test_build_action_registry_ignores_other_enums() {
        let dir = TempDir::new().unwrap();
        let schema_dir = dir.path().join("schema");
        fs::create_dir(&schema_dir).unwrap();

        fs::write(
            schema_dir.join("actions.sb"),
            r#"
// TODO: Update to use new type system (concept/sub_concept)
            "#,
        )
        .unwrap();

        let registry = Project::build_action_registry(dir.path()).unwrap();

        // Action registry is currently disabled during enum removal
        // TODO: Re-implement using concept/sub_concept system
        assert_eq!(registry.len(), 0);
    }
}
