//! Markdown file parsing and document extraction.

use std::path::Path;

use crate::error::SaraError;
use crate::model::{Item, ItemBuilder, ItemId, SourceLocation};
use crate::parser::frontmatter::extract_frontmatter;
use crate::parser::yaml::parse_yaml_frontmatter;
use crate::schema;

/// Parses a Markdown file and extracts the item.
///
/// The file must contain YAML frontmatter delimited by `---`.
pub fn parse_markdown_file(
    content: &str,
    file_path: &Path,
    repository: &Path,
) -> Result<Item, SaraError> {
    let yaml = extract_frontmatter(content, file_path)?;

    let frontmatter = parse_yaml_frontmatter(&yaml, file_path)?;

    // Validate item ID format
    let item_id = ItemId::new(&frontmatter.id).map_err(|e| SaraError::InvalidFrontmatter {
        file: file_path.to_path_buf(),
        reason: format!("Invalid item ID: {}", e),
    })?;

    // Create source location
    let source = SourceLocation::new(repository, file_path);

    // Build the item
    let mut builder = ItemBuilder::new()
        .id(item_id)
        .item_type(frontmatter.item_type)
        .name(&frontmatter.name)
        .source(source)
        .relationships(frontmatter.to_relationships());

    if let Some(desc) = &frontmatter.description {
        builder = builder.description(desc);
    }

    // Populate the attributes the schema declares for this type.
    if let Some(def) = schema::item_type_def(frontmatter.item_type.as_str()) {
        for field in &def.fields {
            match frontmatter.declared_field_value(field) {
                Ok(Some(value)) => builder = builder.attribute(field.name.clone(), value),
                Ok(None) => {}
                Err(reason) => {
                    return Err(SaraError::InvalidFrontmatter {
                        file: file_path.to_path_buf(),
                        reason: format!("field `{}`: {reason}", field.name),
                    });
                }
            }
        }
    }

    builder.build().map_err(|e| SaraError::InvalidFrontmatter {
        file: file_path.to_path_buf(),
        reason: e.to_string(),
    })
}

/// Extracts a name from a markdown file's first heading.
pub fn extract_name_from_content(content: &str) -> Option<String> {
    for line in content.lines() {
        let trimmed = line.trim();
        if let Some(heading) = trimmed.strip_prefix("# ") {
            return Some(heading.trim().to_string());
        }
    }
    None
}

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

    use super::*;

    use crate::model::FieldValue;
    use crate::schema::builtin;

    const SOLUTION_MD: &str = r#"---
id: "SOL-001"
type: solution
name: "Test Solution"
description: "A test solution"
is_refined_by:
  - "UC-001"
---
# Test Solution

This is the body content.
"#;

    const REQUIREMENT_MD: &str = r#"---
id: "SYSREQ-001"
type: system_requirement
name: "Performance Requirement"
specification: "The system SHALL respond within 100ms."
derives_from:
  - "SCEN-001"
is_satisfied_by:
  - "SYSARCH-001"
---
# Requirement
"#;

    #[test]
    fn test_parse_solution() {
        let item = parse_markdown_file(
            SOLUTION_MD,
            &PathBuf::from("SOL-001.md"),
            &PathBuf::from("/repo"),
        )
        .unwrap();

        assert_eq!(item.id.as_str(), "SOL-001");
        assert_eq!(item.item_type, builtin::SOLUTION);
        assert_eq!(item.name, "Test Solution");
        assert_eq!(item.description, Some("A test solution".to_string()));
        let is_refined_by: Vec<_> = item.relationship_ids(builtin::IS_REFINED_BY).collect();
        assert_eq!(is_refined_by.len(), 1);
        assert_eq!(is_refined_by[0].as_str(), "UC-001");
    }

    #[test]
    fn test_parse_requirement() {
        let item = parse_markdown_file(
            REQUIREMENT_MD,
            &PathBuf::from("SYSREQ-001.md"),
            &PathBuf::from("/repo"),
        )
        .unwrap();

        assert_eq!(item.id.as_str(), "SYSREQ-001");
        assert_eq!(item.item_type, builtin::SYSTEM_REQUIREMENT);
        assert_eq!(
            item.attributes.get("specification"),
            Some(&FieldValue::text("The system SHALL respond within 100ms."))
        );
        let derives_from: Vec<_> = item.relationship_ids(builtin::DERIVES_FROM).collect();
        let is_satisfied_by: Vec<_> = item.relationship_ids(builtin::IS_SATISFIED_BY).collect();
        assert_eq!(derives_from.len(), 1);
        assert_eq!(is_satisfied_by.len(), 1);
    }

    #[test]
    fn test_parse_invalid_id() {
        let content = r#"---
id: "invalid id with spaces"
type: solution
name: "Test"
---
"#;
        let result =
            parse_markdown_file(content, &PathBuf::from("test.md"), &PathBuf::from("/repo"));
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_missing_type() {
        let content = r#"---
id: "SOL-001"
name: "Test"
---
"#;
        let result =
            parse_markdown_file(content, &PathBuf::from("test.md"), &PathBuf::from("/repo"));
        assert!(result.is_err());
    }

    const ADR_MD: &str = r#"---
id: "ADR-001"
type: architecture_decision_record
name: "Use Microservices Architecture"
description: "Decision to adopt microservices"
status: proposed
deciders:
  - "Alice Smith"
  - "Bob Jones"
justifies:
  - "SYSARCH-001"
  - "SWDD-001"
supersedes: []
superseded_by: null
---
# Architecture Decision: Use Microservices Architecture

## Context and problem statement

We need to choose an architecture pattern for our system.

## Decision Outcome

Chosen option: Microservices, because it provides better scalability.
"#;

    #[test]
    fn test_parse_adr() {
        let item = parse_markdown_file(
            ADR_MD,
            &PathBuf::from("ADR-001.md"),
            &PathBuf::from("/repo"),
        )
        .unwrap();

        assert_eq!(item.id.as_str(), "ADR-001");
        assert_eq!(item.item_type, builtin::ARCHITECTURE_DECISION_RECORD);
        assert_eq!(item.name, "Use Microservices Architecture");
        assert_eq!(
            item.description,
            Some("Decision to adopt microservices".to_string())
        );

        assert_eq!(
            item.attributes.get("status"),
            Some(&FieldValue::Enum("proposed".to_string()))
        );
        assert_eq!(
            item.attributes.get("deciders"),
            Some(&FieldValue::text_list(["Alice Smith", "Bob Jones"]))
        );
        let justifies: Vec<_> = item.relationship_ids(builtin::JUSTIFIES).collect();
        assert_eq!(justifies.len(), 2);
        assert_eq!(justifies[0].as_str(), "SYSARCH-001");
        assert_eq!(justifies[1].as_str(), "SWDD-001");
        assert!(item.relationship_ids(builtin::SUPERSEDES).next().is_none());
    }

    #[test]
    fn test_parse_adr_missing_deciders() {
        let content = r#"---
id: "ADR-002"
type: architecture_decision_record
name: "Test Decision"
status: proposed
---
"#;
        let result =
            parse_markdown_file(content, &PathBuf::from("test.md"), &PathBuf::from("/repo"));
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_adr_missing_status() {
        let content = r#"---
id: "ADR-003"
type: architecture_decision_record
name: "Test Decision"
deciders:
  - "Alice"
---
"#;
        let result =
            parse_markdown_file(content, &PathBuf::from("test.md"), &PathBuf::from("/repo"));
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_adr_with_supersession() {
        let content = r#"---
id: "ADR-005"
type: architecture_decision_record
name: "Updated Architecture Decision"
status: accepted
deciders:
  - "Alice Smith"
justifies:
  - "SYSARCH-001"
supersedes:
  - "ADR-001"
  - "ADR-002"
---
"#;
        let item = parse_markdown_file(
            content,
            &PathBuf::from("ADR-005.md"),
            &PathBuf::from("/repo"),
        )
        .unwrap();

        let justifies: Vec<_> = item.relationship_ids(builtin::JUSTIFIES).collect();
        assert_eq!(justifies.len(), 1);
        assert_eq!(justifies[0].as_str(), "SYSARCH-001");
        let supersedes: Vec<_> = item.relationship_ids(builtin::SUPERSEDES).collect();
        assert_eq!(supersedes.len(), 2);
        assert_eq!(supersedes[0].as_str(), "ADR-001");
        assert_eq!(supersedes[1].as_str(), "ADR-002");
    }

    #[test]
    fn test_extract_name_from_content() {
        let content = "# My Document\n\nSome content here.";
        assert_eq!(
            extract_name_from_content(content),
            Some("My Document".to_string())
        );

        let content_no_heading = "No heading here";
        assert_eq!(extract_name_from_content(content_no_heading), None);
    }
}
