"""Configuration file manager for Ghostty Settings TUI."""

import os
import re
import shutil
from pathlib import Path
from typing import Optional

from .models import ConfigValue


class ConfigError(Exception):
    """Base exception for config operations."""
    pass


class ConfigReadError(ConfigError):
    """Raised when config cannot be read."""
    pass


class ConfigWriteError(ConfigError):
    """Raised when config cannot be written."""
    pass


class ConfigManager:
    """Manages reading and writing Ghostty configuration files."""

    OPTION_PATTERN = re.compile(r"^([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*(.*)$")
    COMMENTED_OPTION_PATTERN = re.compile(r"^#\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*(.*)$")

    def __init__(self, config_path: Optional[str | Path] = None):
        """Initialize config manager with optional custom path."""
        self._config_path = self._resolve_path(config_path)
        self._original_lines: list[str] = []
        self._values: dict[str, ConfigValue] = {}
        self._last_mtime: float = 0.0

    def _resolve_path(self, config_path: Optional[str | Path]) -> Path:
        """Resolve config file path, respecting XDG_CONFIG_HOME."""
        if config_path:
            return Path(config_path).expanduser()

        # Check XDG_CONFIG_HOME first
        xdg_config = os.environ.get("XDG_CONFIG_HOME")
        if xdg_config:
            return Path(xdg_config) / "ghostty" / "config"

        # Default to ~/.config/ghostty/config
        return Path.home() / ".config" / "ghostty" / "config"

    @property
    def config_path(self) -> Path:
        """Get the config file path."""
        return self._config_path

    @property
    def config_exists(self) -> bool:
        """Check if config file exists."""
        return self._config_path.exists()

    def load(self) -> dict[str, ConfigValue]:
        """Load configuration from file."""
        self._values.clear()
        self._original_lines.clear()

        if not self.config_exists:
            return self._values

        try:
            with open(self._config_path, "r", encoding="utf-8") as f:
                self._original_lines = f.readlines()
            self._last_mtime = self._config_path.stat().st_mtime
        except PermissionError as e:
            raise ConfigReadError(f"Permission denied: {self._config_path}") from e
        except OSError as e:
            raise ConfigReadError(f"Cannot read config: {e}") from e

        for line_num, line in enumerate(self._original_lines, start=1):
            stripped = line.strip()

            # Skip empty lines and pure comments
            if not stripped or (stripped.startswith("#") and "=" not in stripped):
                continue

            # Check for commented-out option
            if match := self.COMMENTED_OPTION_PATTERN.match(stripped):
                name, value = match.group(1), match.group(2).strip()
                self._values[name] = ConfigValue(
                    name=name,
                    value=value if value else None,
                    line_number=line_num,
                    is_commented_out=True
                )
                continue

            # Check for active option
            if match := self.OPTION_PATTERN.match(stripped):
                name, value = match.group(1), match.group(2).strip()

                # Check for inline comment
                inline_comment = None
                if "#" in value and not value.startswith("#"):
                    # Split carefully - don't split hex colors
                    parts = value.split("#", 1)
                    if len(parts) == 2 and not re.match(r"^[0-9a-fA-F]{6}$", parts[1][:6] if len(parts[1]) >= 6 else ""):
                        value = parts[0].strip()
                        inline_comment = parts[1].strip()

                self._values[name] = ConfigValue(
                    name=name,
                    value=value if value else None,
                    line_number=line_num,
                    comment=inline_comment
                )

        return self._values

    def get(self, name: str) -> Optional[ConfigValue]:
        """Get a specific configuration value."""
        return self._values.get(name)

    def set(self, name: str, value: Optional[str]) -> None:
        """Set a configuration value."""
        if name in self._values:
            old_value = self._values[name].value
            self._values[name].value = value
            self._values[name].is_modified = (old_value != value)
            self._values[name].is_commented_out = False
        else:
            self._values[name] = ConfigValue(
                name=name,
                value=value,
                is_modified=True
            )

    def save(self, backup: bool = True) -> None:
        """Save configuration to file, preserving structure and comments."""
        # Create backup
        if backup and self.config_exists:
            backup_path = self._config_path.with_suffix(".config.bak")
            try:
                shutil.copy2(self._config_path, backup_path)
            except OSError:
                pass  # Backup failure is not critical

        # Ensure parent directory exists
        self._config_path.parent.mkdir(parents=True, exist_ok=True)

        # Build new content
        new_lines: list[str] = []
        written_keys: set[str] = set()

        # Process original lines
        for line in self._original_lines:
            stripped = line.strip()

            # Check if line is an option we have a value for
            is_option = False
            for pattern in [self.OPTION_PATTERN, self.COMMENTED_OPTION_PATTERN]:
                if match := pattern.match(stripped):
                    name = match.group(1)
                    if name in self._values:
                        cv = self._values[name]
                        written_keys.add(name)

                        if cv.value is not None and not cv.is_commented_out:
                            # Write the option with new value
                            comment_part = f"  # {cv.comment}" if cv.comment else ""
                            new_lines.append(f"{name} = {cv.value}{comment_part}\n")
                        elif cv.is_commented_out:
                            # Keep it commented
                            value_part = cv.value if cv.value else ""
                            new_lines.append(f"# {name} = {value_part}\n")
                        # If value is None and not commented, skip (removes the option)

                        is_option = True
                        break

            if not is_option:
                # Keep non-option lines as-is
                new_lines.append(line if line.endswith("\n") else line + "\n")

        # Append new options that weren't in original file
        new_options = [
            name for name in self._values
            if name not in written_keys
            and self._values[name].value is not None
            and not self._values[name].is_commented_out
        ]

        if new_options:
            new_lines.append("\n# Added by ghostty-tui\n")
            for name in sorted(new_options):
                cv = self._values[name]
                new_lines.append(f"{name} = {cv.value}\n")

        # Write to file
        try:
            with open(self._config_path, "w", encoding="utf-8") as f:
                f.writelines(new_lines)

            # Reset modification flags
            for cv in self._values.values():
                cv.is_modified = False

            self._original_lines = new_lines
            self._last_mtime = self._config_path.stat().st_mtime
        except PermissionError as e:
            raise ConfigWriteError(f"Permission denied: {self._config_path}") from e
        except OSError as e:
            raise ConfigWriteError(f"Cannot write config: {e}") from e

    def get_mtime(self) -> float:
        """Get modification time of config file."""
        if self.config_exists:
            return self._config_path.stat().st_mtime
        return 0.0

    def has_external_changes(self) -> bool:
        """Check if config was modified externally since last load."""
        return self.get_mtime() > self._last_mtime

    @property
    def has_unsaved_changes(self) -> bool:
        """Check if any values have been modified."""
        return any(v.is_modified for v in self._values.values())

    def get_preview(self) -> str:
        """Get a preview of what the config file will look like after save.

        Returns:
            String containing the preview of the config file content
        """
        preview_lines: list[str] = []
        written_keys: set[str] = set()

        # Process original lines
        for line in self._original_lines:
            stripped = line.strip()

            # Check if line is an option we have a value for
            is_option = False
            for pattern in [self.OPTION_PATTERN, self.COMMENTED_OPTION_PATTERN]:
                if match := pattern.match(stripped):
                    name = match.group(1)
                    if name in self._values:
                        cv = self._values[name]
                        written_keys.add(name)

                        if cv.value is not None and not cv.is_commented_out:
                            comment_part = f"  # {cv.comment}" if cv.comment else ""
                            preview_lines.append(f"{name} = {cv.value}{comment_part}")
                        elif cv.is_commented_out:
                            value_part = cv.value if cv.value else ""
                            preview_lines.append(f"# {name} = {value_part}")

                        is_option = True
                        break

            if not is_option:
                # Keep non-option lines as-is
                preview_lines.append(line.rstrip("\n"))

        # Append new options that weren't in original file
        new_options = [
            name for name in self._values
            if name not in written_keys
            and self._values[name].value is not None
            and not self._values[name].is_commented_out
        ]

        if new_options:
            preview_lines.append("")
            preview_lines.append("# Added by ghostty-tui")
            for name in sorted(new_options):
                cv = self._values[name]
                preview_lines.append(f"{name} = {cv.value}")

        return "\n".join(preview_lines)
