"""Parser for Ghostty configuration schema."""

import re
import subprocess
from pathlib import Path
from typing import Optional

from .models import OptionDefinition, OptionType
from .categories import infer_category


class GhosttyParserError(Exception):
    """Raised when schema parsing fails."""
    pass


class GhosttyNotFoundError(GhosttyParserError):
    """Raised when Ghostty executable is not found."""
    pass


class GhosttyParser:
    """Parses Ghostty configuration schema from CLI output."""

    # Regex patterns
    COMMENT_PATTERN = re.compile(r"^#\s*(.*)$")
    OPTION_PATTERN = re.compile(r"^([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*(.*)$")
    ENUM_PATTERN = re.compile(r"[Vv]alid values?:\s*(.+?)(?:\.|$)", re.IGNORECASE)
    KEYBIND_PATTERN = re.compile(r"^keybind\s*=")

    def __init__(self, ghostty_path: str = "ghostty"):
        """Initialize parser with ghostty executable path."""
        self.ghostty_path = ghostty_path
        self._raw_output: Optional[str] = None

    def fetch_schema(self, timeout: float = 10.0) -> str:
        """Execute ghostty command and capture schema output."""
        try:
            result = subprocess.run(
                [self.ghostty_path, "+show-config", "--default", "--docs"],
                capture_output=True,
                text=True,
                timeout=timeout,
            )

            if result.returncode != 0:
                # Try without --docs flag (older versions)
                result = subprocess.run(
                    [self.ghostty_path, "+show-config", "--default"],
                    capture_output=True,
                    text=True,
                    timeout=timeout,
                )

            self._raw_output = result.stdout
            return self._raw_output

        except subprocess.TimeoutExpired as e:
            raise GhosttyParserError(f"Ghostty command timed out after {timeout}s") from e
        except FileNotFoundError as e:
            raise GhosttyNotFoundError(
                "Ghostty executable not found. Please ensure Ghostty is installed and in PATH."
            ) from e
        except OSError as e:
            raise GhosttyParserError(f"Failed to run Ghostty: {e}") from e

    def parse(self, raw_output: Optional[str] = None) -> list[OptionDefinition]:
        """Parse raw output into OptionDefinition list."""
        content = raw_output or self._raw_output
        if not content:
            raise GhosttyParserError("No content to parse. Call fetch_schema() first.")

        options: list[OptionDefinition] = []
        current_docs: list[str] = []

        for line in content.splitlines():
            line = line.rstrip()

            # Skip empty lines
            if not line:
                # Reset doc accumulator on blank line after non-comment
                if current_docs and not current_docs[-1].strip():
                    current_docs = []
                continue

            # Accumulate doc comments
            if match := self.COMMENT_PATTERN.match(line):
                doc_line = match.group(1)
                current_docs.append(doc_line)
                continue

            # Parse option line
            if match := self.OPTION_PATTERN.match(line):
                name, default = match.group(1), match.group(2).strip()

                try:
                    option = self._create_option(name, default, current_docs)
                    options.append(option)
                except Exception:
                    pass  # Skip malformed options

                current_docs = []

        return options

    def parse_from_file(self, file_path: str | Path) -> list[OptionDefinition]:
        """Parse schema from a file (for testing)."""
        path = Path(file_path)
        if not path.exists():
            raise GhosttyParserError(f"Schema file not found: {path}")

        content = path.read_text(encoding="utf-8")
        return self.parse(content)

    def _create_option(
        self,
        name: str,
        default: str,
        docs: list[str]
    ) -> OptionDefinition:
        """Create OptionDefinition from parsed components."""
        description = "\n".join(docs).strip()
        option_type = self._infer_type(name, default, description)
        enum_values = self._extract_enum_values(description) if option_type == OptionType.ENUM else ()

        # Determine if deprecated
        is_deprecated = "deprecated" in description.lower()

        return OptionDefinition(
            name=name,
            option_type=option_type,
            default_value=default if default else None,
            description=description,
            category=infer_category(name),
            enum_values=tuple(enum_values),
            is_deprecated=is_deprecated,
        )

    def _infer_type(self, name: str, default: str, docs: str) -> OptionType:
        """Infer option type from name, default value, and documentation."""
        name_lower = name.lower()
        default_lower = default.lower()
        docs_lower = docs.lower()

        # Check for keybind type
        if "keybind" in name_lower:
            return OptionType.KEYBIND

        # Check documentation for enum indicators
        if "valid values:" in docs_lower or "valid value:" in docs_lower:
            # Only if values are listed (not just mentioned)
            if self._extract_enum_values(docs):
                return OptionType.ENUM

        # Check for boolean
        if default_lower in ("true", "false"):
            return OptionType.BOOLEAN

        # Check for color (hex format or color-related name)
        if default.startswith("#") and len(default) in (4, 7):
            return OptionType.COLOR
        if re.match(r"^[0-9a-fA-F]{6}$", default):
            return OptionType.COLOR
        if "color" in name_lower:
            return OptionType.COLOR

        # Check for path
        if "path" in name_lower or "file" in name_lower or "directory" in name_lower:
            return OptionType.PATH
        if default.startswith("/") or default.startswith("~"):
            return OptionType.PATH

        # Check for numbers
        if default:
            # Float check
            if re.match(r"^-?\d*\.\d+$", default):
                return OptionType.FLOAT
            # Integer check
            if re.match(r"^-?\d+$", default):
                return OptionType.INTEGER

        return OptionType.STRING

    def _extract_enum_values(self, docs: str) -> list[str]:
        """Extract enum values from documentation."""
        if match := self.ENUM_PATTERN.search(docs):
            values_str = match.group(1)
            # Parse comma-separated values
            values = []
            for v in values_str.split(","):
                v = v.strip().strip("\"'`")
                # Stop at sentence boundaries
                if v and not v.startswith("or "):
                    # Remove "or" prefix if present
                    v = re.sub(r"^or\s+", "", v)
                    # Only accept short, simple values (real enum values)
                    # Skip prose like "any positive integer" or long descriptions
                    if len(v) <= 30 and " " not in v or v in ("true", "false"):
                        values.append(v)
            # Only return if we have at least 2 distinct values (real enum)
            if len(values) >= 2:
                return values
        return []


def get_schema(ghostty_path: str = "ghostty") -> dict[str, OptionDefinition]:
    """Convenience function to get schema as a dictionary."""
    parser = GhosttyParser(ghostty_path)
    parser.fetch_schema()
    options = parser.parse()
    return {opt.name: opt for opt in options}
