"""Document builder for creating ClaudeDocument objects from snippets."""

import json
import logging
from pathlib import Path
from typing import Any

from .models import Agent, ClaudeDocument, Command, Hook, Principle, Section
from .utils import (
    parse_agents_with_frontmatter,
    parse_principles_with_frontmatter,
    parse_section_with_frontmatter,
    validate_file_size,
)

# Configure logging
logger = logging.getLogger(__name__)


class DocumentBuilder:
    """Handles building ClaudeDocument objects from markdown snippets."""

    def __init__(self, snippets_dir: Path):
        """Initialize the document builder.

        Args:
            snippets_dir: Directory containing snippet files
        """
        self.snippets_dir = Path(snippets_dir)
        logger.info(f"Initializing DocumentBuilder with snippets_dir={snippets_dir}")

    def load_snippet(self, snippet_path: str) -> str:
        """Load a markdown snippet from the snippets directory."""
        logger.debug(f"Loading snippet: {snippet_path}")

        # Normalize path to prevent directory traversal
        normalized_path = Path(snippet_path)

        # Check for path traversal attempts
        if ".." in normalized_path.parts or normalized_path.is_absolute():
            logger.warning(f"Path traversal attempt detected: {snippet_path}")
            raise ValueError(f"Path traversal attempt detected: {snippet_path}")

        full_path = (self.snippets_dir / normalized_path).resolve()

        # Ensure resolved path is within snippets directory
        if not str(full_path).startswith(str(self.snippets_dir.resolve())):
            logger.warning(f"Path traversal attempt detected: {snippet_path}")
            raise ValueError(f"Path traversal attempt detected: {snippet_path}")

        if not full_path.exists():
            logger.error(f"Snippet not found: {full_path}")
            raise FileNotFoundError(f"Snippet not found: {full_path}")

        # Validate file size before reading
        try:
            validate_file_size(full_path)
            logger.debug(f"File size validation passed for: {full_path}")
        except ValueError as e:
            logger.error(f"File size validation failed for {full_path}: {e}")
            raise

        content = full_path.read_text(encoding="utf-8")
        logger.debug(
            f"Successfully loaded snippet: {snippet_path} ({len(content)} chars)"
        )
        return content

    def load_agents(self) -> list[Agent]:
        """Load agent definitions from `agents.md` or fallback to `agents/` dir."""
        agents: list[Agent] = []
        single_file = self.snippets_dir / "agents.md"
        if single_file.exists():
            content = single_file.read_text(encoding="utf-8")
            agent_dicts = parse_agents_with_frontmatter(content)
            for agent_dict in agent_dicts:
                agent = Agent(**agent_dict)
                agents.append(agent)
            return agents

        # Fallback to directory mode for backward compatibility
        agents_dir = self.snippets_dir / "agents"
        if agents_dir.exists():
            for agent_file in agents_dir.glob("*.md"):
                content = agent_file.read_text(encoding="utf-8")
                agent_dicts = parse_agents_with_frontmatter(content)
                for agent_dict in agent_dicts:
                    # Override name with filename if not in frontmatter
                    if not agent_dict.get("name"):
                        agent_dict["name"] = agent_file.stem.replace("_", " ").title()
                    agent = Agent(**agent_dict)
                    agents.append(agent)
        return agents

    def load_agents_markdown(self) -> str:
        """Load raw agents markdown for template rendering."""
        single_file = self.snippets_dir / "agents.md"
        if single_file.exists():
            return single_file.read_text(encoding="utf-8")

        # Generate markdown from individual agent files
        agents_dir = self.snippets_dir / "agents"
        if agents_dir.exists():
            markdown_parts = []
            for agent_file in sorted(agents_dir.glob("*.md")):
                content = agent_file.read_text(encoding="utf-8")

                # Parse frontmatter to get agent name for heading
                agent_dicts = parse_agents_with_frontmatter(content)
                if agent_dicts:
                    agent_name = agent_dicts[0].get(
                        "name", agent_file.stem.replace("_", " ").title()
                    )
                    # Strip frontmatter and add heading
                    if content.strip().startswith("---"):
                        parts = content.split("---", 2)
                        if len(parts) >= 3:
                            markdown_content = parts[2].strip()
                        else:
                            markdown_content = content
                    else:
                        markdown_content = content

                    # Only add heading if not already present in content
                    if not markdown_content.strip().startswith(f"### {agent_name}"):
                        markdown_parts.append(f"### {agent_name}")
                    markdown_parts.append(markdown_content)
                    markdown_parts.append("")  # Empty line between agents

            return "\n".join(markdown_parts)

        return ""

    def load_principles(self) -> list[Principle]:
        """Load principles from `fundamental_principles.md` or directory fallback."""
        principles: list[Principle] = []
        single_file = self.snippets_dir / "fundamental_principles.md"
        if single_file.exists():
            content = single_file.read_text(encoding="utf-8")
            principle_dicts = parse_principles_with_frontmatter(content)
            for principle_dict in principle_dicts:
                principle = Principle(**principle_dict)
                principles.append(principle)
            return principles

        # Directory fallback
        principles_dir = self.snippets_dir / "fundamental_principles"
        if principles_dir.exists():
            for principle_file in principles_dir.glob("*.md"):
                content = principle_file.read_text(encoding="utf-8")
                principle_dicts = parse_principles_with_frontmatter(content)
                for principle_dict in principle_dicts:
                    # Override title with filename if not in frontmatter
                    if not principle_dict.get("title"):
                        principle_dict["title"] = principle_file.stem.replace(
                            "_", " "
                        ).title()
                    principle = Principle(**principle_dict)
                    principles.append(principle)
        return principles

    def load_communication_patterns(self) -> dict[str, str]:
        """Load communication patterns."""
        patterns = {}
        # First, single-file fallback for backward compatibility
        single_file = self.snippets_dir / "communication_patterns.md"
        if single_file.exists():
            content = single_file.read_text(encoding="utf-8")
            # Use a generic key if only one file exists
            patterns["communication_patterns"] = content
            return patterns

        patterns_dir = self.snippets_dir / "communication_patterns"
        if patterns_dir.exists():
            for pattern_file in patterns_dir.glob("*.md"):
                key = pattern_file.stem
                content = pattern_file.read_text(encoding="utf-8")
                patterns[key] = content

        return patterns

    def load_validation_system(self) -> dict[str, str]:
        """Load validation system components."""
        validation = {}
        # Single-file fallback
        single_file = self.snippets_dir / "validation_system.md"
        if single_file.exists():
            content = single_file.read_text(encoding="utf-8")
            validation["validation_system"] = content
            return validation

        validation_dir = self.snippets_dir / "validation_system"
        if validation_dir.exists():
            for validation_file in validation_dir.glob("*.md"):
                key = validation_file.stem
                content = validation_file.read_text(encoding="utf-8")
                validation[key] = content

        return validation

    def load_settings(self) -> dict[str, Any]:
        """Load settings from settings.json."""

        settings_file = self.snippets_dir / "settings.json"
        if settings_file.exists():
            try:
                content = settings_file.read_text(encoding="utf-8")
                return json.loads(content)
            except (json.JSONDecodeError, UnicodeDecodeError) as e:
                logger.warning(f"Failed to load settings.json: {e}")
                return {}

        logger.debug("No settings.json found, returning empty settings")
        return {}

    def load_hooks(self) -> list[Hook]:
        """Load hook files from the hooks directory."""
        hooks: list[Hook] = []
        hooks_dir = self.snippets_dir / "hooks"

        if not hooks_dir.exists():
            logger.debug("No hooks directory found")
            return hooks

        for hook_file in hooks_dir.glob("*"):
            if hook_file.is_file():
                content = hook_file.read_text(encoding="utf-8")
                language = "python" if hook_file.suffix == ".py" else "shell"

                hook = Hook(
                    name=hook_file.stem,
                    file_path=hook_file,
                    content=content,
                    language=language,
                )
                hooks.append(hook)
                logger.debug(f"Loaded hook: {hook_file.name}")

        logger.info(f"Loaded {len(hooks)} hooks")
        return hooks

    def load_commands(self) -> list[Command]:
        """Load command files from the commands directory."""
        commands: list[Command] = []
        commands_dir = self.snippets_dir / "commands"

        if not commands_dir.exists():
            logger.debug("No commands directory found")
            return commands

        for command_file in commands_dir.glob("*.md"):
            content = command_file.read_text(encoding="utf-8")

            # Extract description from first line if available
            lines = content.strip().split("\n")
            description = lines[0] if lines else ""

            command = Command(
                name=command_file.stem,
                file_path=command_file,
                content=content,
                description=description,
            )
            commands.append(command)
            logger.debug(f"Loaded command: {command_file.name}")

        logger.info(f"Loaded {len(commands)} commands")
        return commands

    def load_ordered_sections(self) -> list[Section]:
        r"""Load sections from numbered markdown files with frontmatter metadata.

        Scans for files matching pattern: \d+-*.md (e.g., 01-core-mission.md)
        Parses frontmatter for metadata and orders by filename numbers.

        Returns:
            List of Section objects sorted by order
        """
        import re

        sections: list[Section] = []

        # Find all numbered markdown files
        numbered_files = []
        for file_path in self.snippets_dir.glob("*.md"):
            if re.match(r"^\d+-", file_path.name):
                numbered_files.append(file_path)

        if not numbered_files:
            logger.debug("No numbered section files found")
            return sections

        logger.info(f"Found {len(numbered_files)} numbered section files")

        # Process each numbered file
        for file_path in numbered_files:
            try:
                # Validate file size
                validate_file_size(file_path)

                # Read content
                content = file_path.read_text(encoding="utf-8")

                # Parse with frontmatter
                section_data = parse_section_with_frontmatter(file_path.name, content)

                # Create Section object
                section = Section(**section_data)
                sections.append(section)

                logger.debug(f"Loaded section: {file_path.name} -> {section.title}")

            except Exception as e:
                logger.error(f"Failed to load section {file_path.name}: {e}")
                continue

        # Sort by order (uses order_override if available, else filename number)
        sections.sort(key=lambda s: s.order)

        logger.info(f"Successfully loaded {len(sections)} ordered sections")
        return sections

    def build_document(self) -> ClaudeDocument:
        """Build the complete Claude document from snippets.

        Uses ordered sections approach if numbered files exist,
        otherwise falls back to legacy hardcoded approach.
        """
        logger.info("Starting document build process")

        # Try to load ordered sections first (new approach)
        sections = self.load_ordered_sections()

        if sections:
            logger.info(
                f"Using dynamic sections approach with {len(sections)} sections"
            )
            return self._build_document_from_sections(sections)
        else:
            logger.info("No numbered sections found, using legacy hardcoded approach")
            return self._build_document_legacy()

    def _build_document_from_sections(self, sections: list[Section]) -> ClaudeDocument:
        """Build document from ordered sections (new approach)."""
        # Load additional components
        hooks = self.load_hooks()
        commands = self.load_commands()
        settings = self.load_settings()

        # For backward compatibility, also load legacy components
        agents = self.load_agents()
        principles = self.load_principles()

        document = ClaudeDocument(
            sections=sections,
            hooks=hooks,
            commands=commands,
            settings=settings,
            agents=agents,
            principles=principles,
        )

        logger.info("Document built successfully from ordered sections")
        return document

    def _build_document_legacy(self) -> ClaudeDocument:
        """Build document using legacy hardcoded approach."""
        # Load core mission
        core_mission = self.load_snippet("core_mission.md")

        # Load principles
        principles = self.load_principles()
        logger.info(f"Loaded {len(principles)} principles")

        # Load agents list for validation and analytics
        agents = self.load_agents()
        logger.info(f"Loaded {len(agents)} agents")

        # Raw markdown for agents to render exactly as written
        agents_markdown = self.load_agents_markdown()

        # Load other sections
        communication_patterns = self.load_communication_patterns()
        validation_system = self.load_validation_system()

        # Load new sections
        hooks = self.load_hooks()
        commands = self.load_commands()
        settings = self.load_settings()

        document = ClaudeDocument(
            core_mission=core_mission,
            principles=principles,
            agents=agents,
            agents_markdown=agents_markdown,
            communication_patterns=communication_patterns,
            validation_system=validation_system,
            directory_management=self.load_snippet("directory_management.md"),
            workflow_execution=self.load_snippet("workflow_execution.md"),
            key_artifacts=self.load_snippet("key_artifacts.md"),
            token_optimization_rules=self.load_snippet("token_optimization_rules.md"),
            hooks=hooks,
            commands=commands,
            settings=settings,
        )

        logger.info("Document build process completed successfully (legacy mode)")
        return document
