//! Init service implementation.

use std::fs;
use std::path::PathBuf;

use crate::model::{FieldName, ItemType};
use crate::parser::has_frontmatter;
use crate::template::{
    GeneratorOptions, extract_name_from_content, generate_document, generate_frontmatter,
    generate_id,
};

use super::InitOptions;

/// Creates an InvalidOption error with a formatted message.
fn invalid_option_error(field: FieldName, valid_types: &[ItemType], actual: ItemType) -> InitError {
    let valid_names: Vec<_> = valid_types.iter().map(|t| t.display_name()).collect();
    InitError::InvalidOption(format!(
        "{} is only valid for {}, not {}",
        field.as_str(),
        valid_names.join(", "),
        actual.display_name()
    ))
}

/// Errors that can occur during initialization.
#[derive(Debug, thiserror::Error)]
pub enum InitError {
    /// File already has frontmatter and force was not set.
    #[error("File {0} already has frontmatter. Use force to overwrite.")]
    FrontmatterExists(PathBuf),

    /// Invalid option for the given item type.
    #[error("{0}")]
    InvalidOption(String),

    /// IO error.
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

/// Result of a successful initialization.
#[derive(Debug, Clone)]
pub struct InitResult {
    /// The resolved ID.
    pub id: String,
    /// The resolved name.
    pub name: String,
    /// The item type.
    pub item_type: ItemType,
    /// The file path.
    pub file: PathBuf,
    /// Whether an existing file was updated (true) or a new file was created (false).
    pub updated_existing: bool,
    /// Whether frontmatter was replaced (only relevant if updated_existing is true).
    pub replaced_frontmatter: bool,
    /// Whether specification field needs attention.
    pub needs_specification: bool,
}

/// Service for initializing requirement items.
#[derive(Debug, Default)]
pub struct InitService;

impl InitService {
    /// Creates a new init service.
    pub fn new() -> Self {
        Self
    }

    /// Initializes an item based on the provided options.
    ///
    /// This will either create a new file or update an existing file with frontmatter.
    pub fn init(&self, opts: &InitOptions) -> Result<InitResult, InitError> {
        // Validate type-specific options
        self.validate_options(opts)?;

        // Check for existing frontmatter
        if opts.file.exists() && !opts.force {
            let content = fs::read_to_string(&opts.file)?;
            if has_frontmatter(&content) {
                return Err(InitError::FrontmatterExists(opts.file.clone()));
            }
        }

        // Resolve ID and name
        let id = self.resolve_id(opts);
        let name = self.resolve_name(opts, &id)?;

        // Build generator options
        let gen_opts = self.build_generator_options(opts, id.clone(), name.clone());

        // Write the file
        let (updated_existing, replaced_frontmatter) = self.write_file(opts, &gen_opts)?;

        Ok(InitResult {
            id,
            name,
            item_type: opts.item_type,
            file: opts.file.clone(),
            updated_existing,
            replaced_frontmatter,
            needs_specification: opts.item_type.requires_specification()
                && opts.specification.is_none(),
        })
    }

    /// Validates type-specific options.
    fn validate_options(&self, opts: &InitOptions) -> Result<(), InitError> {
        if !opts.refines.is_empty() && !opts.item_type.requires_refines() {
            return Err(invalid_option_error(
                FieldName::Refines,
                ItemType::refines_types(),
                opts.item_type,
            ));
        }
        if !opts.derives_from.is_empty() && !opts.item_type.requires_derives_from() {
            return Err(invalid_option_error(
                FieldName::DerivesFrom,
                ItemType::derives_from_types(),
                opts.item_type,
            ));
        }
        if !opts.satisfies.is_empty() && !opts.item_type.requires_satisfies() {
            return Err(invalid_option_error(
                FieldName::Satisfies,
                ItemType::satisfies_types(),
                opts.item_type,
            ));
        }
        if opts.specification.is_some() && !opts.item_type.requires_specification() {
            return Err(invalid_option_error(
                FieldName::Specification,
                ItemType::specification_types(),
                opts.item_type,
            ));
        }
        if opts.platform.is_some() && !opts.item_type.accepts_platform() {
            return Err(invalid_option_error(
                FieldName::Platform,
                ItemType::platform_types(),
                opts.item_type,
            ));
        }
        Ok(())
    }

    /// Resolves the ID from options or generates a new one.
    fn resolve_id(&self, opts: &InitOptions) -> String {
        opts.id
            .clone()
            .unwrap_or_else(|| generate_id(opts.item_type, None))
    }

    /// Resolves the name from options, file content, or file stem.
    fn resolve_name(&self, opts: &InitOptions, id: &str) -> Result<String, InitError> {
        if let Some(ref name) = opts.name {
            return Ok(name.clone());
        }

        if opts.file.exists() {
            let content = fs::read_to_string(&opts.file)?;
            if let Some(name) = extract_name_from_content(&content) {
                return Ok(name);
            }
        }

        Ok(self.file_stem_or_fallback(&opts.file, id))
    }

    /// Returns the file stem as a string, or the fallback if unavailable.
    fn file_stem_or_fallback(&self, file: &std::path::Path, fallback: &str) -> String {
        file.file_stem()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_else(|| fallback.to_string())
    }

    /// Builds generator options from init options.
    fn build_generator_options(
        &self,
        opts: &InitOptions,
        id: String,
        name: String,
    ) -> GeneratorOptions {
        let mut gen_opts = GeneratorOptions::new(opts.item_type, id, name);

        if let Some(ref desc) = opts.description {
            gen_opts = gen_opts.with_description(desc);
        }
        if !opts.refines.is_empty() {
            gen_opts = gen_opts.with_refines(opts.refines.clone());
        }
        if !opts.derives_from.is_empty() {
            gen_opts = gen_opts.with_derives_from(opts.derives_from.clone());
        }
        if !opts.satisfies.is_empty() {
            gen_opts = gen_opts.with_satisfies(opts.satisfies.clone());
        }
        if let Some(ref spec) = opts.specification {
            gen_opts = gen_opts.with_specification(spec);
        }
        if let Some(ref platform) = opts.platform {
            gen_opts = gen_opts.with_platform(platform);
        }

        gen_opts
    }

    /// Writes the file, either updating existing or creating new.
    /// Returns (updated_existing, replaced_frontmatter).
    fn write_file(
        &self,
        opts: &InitOptions,
        gen_opts: &GeneratorOptions,
    ) -> Result<(bool, bool), InitError> {
        if opts.file.exists() {
            let replaced = self.update_existing_file(opts, gen_opts)?;
            Ok((true, replaced))
        } else {
            self.create_new_file(opts, gen_opts)?;
            Ok((false, false))
        }
    }

    /// Updates an existing file by adding or replacing frontmatter.
    /// Returns true if frontmatter was replaced, false if it was added.
    fn update_existing_file(
        &self,
        opts: &InitOptions,
        gen_opts: &GeneratorOptions,
    ) -> Result<bool, InitError> {
        let content = fs::read_to_string(&opts.file)?;
        let frontmatter = generate_frontmatter(gen_opts);

        let (new_content, replaced) = if has_frontmatter(&content) && opts.force {
            let body = remove_frontmatter(&content);
            (format!("{}\n{}", frontmatter, body), true)
        } else {
            (format!("{}\n{}", frontmatter, content), false)
        };

        fs::write(&opts.file, new_content)?;
        Ok(replaced)
    }

    /// Creates a new file with the generated document.
    fn create_new_file(
        &self,
        opts: &InitOptions,
        gen_opts: &GeneratorOptions,
    ) -> Result<(), InitError> {
        let document = generate_document(gen_opts);

        if let Some(parent) = opts.file.parent() {
            fs::create_dir_all(parent)?;
        }

        fs::write(&opts.file, document)?;
        Ok(())
    }
}

/// Removes frontmatter from content.
fn remove_frontmatter(content: &str) -> &str {
    let mut in_frontmatter = false;
    let mut frontmatter_end = 0;

    for (i, line) in content.lines().enumerate() {
        if line.trim() == "---" {
            if !in_frontmatter {
                in_frontmatter = true;
            } else {
                // Found end of frontmatter
                frontmatter_end = content
                    .lines()
                    .take(i + 1)
                    .map(|l| l.len() + 1)
                    .sum::<usize>();
                break;
            }
        }
    }

    if frontmatter_end > 0 && frontmatter_end < content.len() {
        &content[frontmatter_end..]
    } else {
        content
    }
}

/// Parses an item type string into ItemType enum.
pub fn parse_item_type(type_str: &str) -> Option<ItemType> {
    match type_str.to_lowercase().as_str() {
        "solution" | "sol" => Some(ItemType::Solution),
        "use_case" | "usecase" | "uc" => Some(ItemType::UseCase),
        "scenario" | "scen" => Some(ItemType::Scenario),
        "system_requirement" | "systemrequirement" | "sysreq" => Some(ItemType::SystemRequirement),
        "system_architecture" | "systemarchitecture" | "sysarch" => {
            Some(ItemType::SystemArchitecture)
        }
        "hardware_requirement" | "hardwarerequirement" | "hwreq" => {
            Some(ItemType::HardwareRequirement)
        }
        "software_requirement" | "softwarerequirement" | "swreq" => {
            Some(ItemType::SoftwareRequirement)
        }
        "hardware_detailed_design" | "hardwaredetaileddesign" | "hwdd" => {
            Some(ItemType::HardwareDetailedDesign)
        }
        "software_detailed_design" | "softwaredetaileddesign" | "swdd" => {
            Some(ItemType::SoftwareDetailedDesign)
        }
        _ => None,
    }
}

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

    #[test]
    fn test_parse_item_type() {
        assert_eq!(parse_item_type("solution"), Some(ItemType::Solution));
        assert_eq!(parse_item_type("SOL"), Some(ItemType::Solution));
        assert_eq!(parse_item_type("use_case"), Some(ItemType::UseCase));
        assert_eq!(parse_item_type("UC"), Some(ItemType::UseCase));
        assert_eq!(parse_item_type("invalid"), None);
    }

    #[test]
    fn test_remove_frontmatter() {
        let content = "---\nid: test\n---\n# Body";
        let body = remove_frontmatter(content);
        assert_eq!(body.trim(), "# Body");
    }

    #[test]
    fn test_init_new_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.md");

        let opts = InitOptions::new(file_path.clone(), ItemType::Solution)
            .with_id("SOL-001")
            .with_name("Test Solution");

        let service = InitService::new();
        let result = service.init(&opts).unwrap();

        assert_eq!(result.id, "SOL-001");
        assert_eq!(result.name, "Test Solution");
        assert!(!result.updated_existing);
        assert!(file_path.exists());

        let content = fs::read_to_string(&file_path).unwrap();
        assert!(content.contains("id: \"SOL-001\""));
        assert!(content.contains("# Solution: Test Solution"));
    }

    #[test]
    fn test_init_existing_file_without_frontmatter() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("existing.md");

        // Create existing file without frontmatter
        fs::write(&file_path, "# My Document\n\nSome content here.").unwrap();

        let opts = InitOptions::new(file_path.clone(), ItemType::UseCase).with_id("UC-001");

        let service = InitService::new();
        let result = service.init(&opts).unwrap();

        assert_eq!(result.id, "UC-001");
        assert_eq!(result.name, "My Document"); // Extracted from heading
        assert!(result.updated_existing);
        assert!(!result.replaced_frontmatter);

        let content = fs::read_to_string(&file_path).unwrap();
        assert!(content.contains("id: \"UC-001\""));
        assert!(content.contains("# My Document"));
    }

    #[test]
    fn test_init_existing_file_with_frontmatter_no_force() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("existing.md");

        // Create existing file with frontmatter
        fs::write(&file_path, "---\nid: OLD-001\n---\n# Content").unwrap();

        let opts = InitOptions::new(file_path, ItemType::Solution).with_id("SOL-001");

        let service = InitService::new();
        let result = service.init(&opts);

        assert!(matches!(result, Err(InitError::FrontmatterExists(_))));
    }

    #[test]
    fn test_init_existing_file_with_frontmatter_force() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("existing.md");

        // Create existing file with frontmatter
        fs::write(&file_path, "---\nid: OLD-001\n---\n# Content").unwrap();

        let opts = InitOptions::new(file_path.clone(), ItemType::Solution)
            .with_id("SOL-001")
            .with_name("New Solution")
            .with_force(true);

        let service = InitService::new();
        let result = service.init(&opts).unwrap();

        assert_eq!(result.id, "SOL-001");
        assert!(result.updated_existing);
        assert!(result.replaced_frontmatter);

        let content = fs::read_to_string(&file_path).unwrap();
        assert!(content.contains("id: \"SOL-001\""));
        assert!(!content.contains("OLD-001"));
    }

    #[test]
    fn test_validate_refines_valid() {
        let temp_dir = TempDir::new().unwrap();
        let opts = InitOptions::new(temp_dir.path().join("test.md"), ItemType::UseCase)
            .with_refines(vec!["SOL-001".to_string()]);

        let service = InitService::new();
        assert!(service.validate_options(&opts).is_ok());
    }

    #[test]
    fn test_validate_refines_invalid() {
        let temp_dir = TempDir::new().unwrap();
        let opts = InitOptions::new(temp_dir.path().join("test.md"), ItemType::SystemRequirement)
            .with_refines(vec!["SOL-001".to_string()]);

        let service = InitService::new();
        assert!(service.validate_options(&opts).is_err());
    }

    #[test]
    fn test_validate_platform_valid() {
        let temp_dir = TempDir::new().unwrap();
        let opts = InitOptions::new(
            temp_dir.path().join("test.md"),
            ItemType::SystemArchitecture,
        )
        .with_platform("AWS");

        let service = InitService::new();
        assert!(service.validate_options(&opts).is_ok());
    }

    #[test]
    fn test_validate_platform_invalid() {
        let temp_dir = TempDir::new().unwrap();
        let opts = InitOptions::new(temp_dir.path().join("test.md"), ItemType::Solution)
            .with_platform("AWS");

        let service = InitService::new();
        assert!(service.validate_options(&opts).is_err());
    }

    #[test]
    fn test_needs_specification() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.md");

        let opts = InitOptions::new(file_path, ItemType::SystemRequirement).with_id("SYSREQ-001");

        let service = InitService::new();
        let result = service.init(&opts).unwrap();

        assert!(result.needs_specification);
    }
}
