"""
Codec layer for format serialization.

This module provides pluggable codecs for different secret formats
(JSON, YAML) with a clean abstract interface.
"""

from __future__ import annotations

import json
from abc import ABC, abstractmethod
from pathlib import Path

from kilit.exceptions import KilitFormatError


class Codec(ABC):
    """
    Abstract interface for serialization formats.

    Codecs handle serialization and deserialization of secrets dictionaries
    to/from string formats (JSON, YAML, etc.).

    All codecs must implement:
    - name: Codec identifier (e.g., "json", "yaml")
    - loads: Parse string to dictionary
    - dumps: Format dictionary to string
    - default_extension: File extension for this format

    Error Handling:
    - All methods should raise KilitFormatError on failures
    - Error messages should be clear and actionable
    """

    @property
    @abstractmethod
    def name(self) -> str:
        """Return codec name (e.g., 'json', 'yaml')."""
        pass

    @abstractmethod
    def loads(self, data: str) -> dict:
        """
        Parse string into dictionary.

        Args:
            data: String to parse

        Returns:
            Parsed dictionary

        Raises:
            KilitFormatError: If parsing fails
        """
        pass

    @abstractmethod
    def dumps(self, data: dict) -> str:
        """
        Format dictionary into string.

        Args:
            data: Dictionary to format

        Returns:
            Formatted string

        Raises:
            KilitFormatError: If formatting fails
        """
        pass

    @property
    @abstractmethod
    def default_extension(self) -> str:
        """Return default file extension (e.g., '.json', '.yml')."""
        pass


class JSONCodec(Codec):
    """
    Built-in JSON codec using standard library.

    Features:
    - Uses Python's json module (no dependencies)
    - Pretty-printed output (indent=2)
    - Sorted keys for deterministic output
    - Clear error messages on parse failures

    Format:
    - Standard JSON format
    - UTF-8 encoding
    - Sorted keys for consistency
    """

    @property
    def name(self) -> str:
        return "json"

    def loads(self, data: str) -> dict:
        """
        Parse JSON string into dictionary.

        Args:
            data: JSON string

        Returns:
            Parsed dictionary

        Raises:
            KilitFormatError: If JSON is invalid
        """
        try:
            result = json.loads(data)
            if not isinstance(result, dict):
                raise KilitFormatError(f"Expected JSON object (dict), got {type(result).__name__}")
            return result
        except json.JSONDecodeError as e:
            raise KilitFormatError(f"Invalid JSON: {e}")
        except Exception as e:
            raise KilitFormatError(f"Failed to parse JSON: {e}")

    def dumps(self, data: dict) -> str:
        """
        Format dictionary into JSON string.

        Args:
            data: Dictionary to format

        Returns:
            Pretty-printed JSON string

        Raises:
            KilitFormatError: If formatting fails
        """
        try:
            return json.dumps(data, indent=2, sort_keys=True, ensure_ascii=False)
        except Exception as e:
            raise KilitFormatError(f"Failed to format JSON: {e}")

    @property
    def default_extension(self) -> str:
        return ".json"


class YAMLCodec(Codec):
    """
    Optional YAML codec (requires PyYAML).

    Features:
    - Uses PyYAML's safe_load/safe_dump (no code execution)
    - Pretty-printed output (default_flow_style=False)
    - Sorted keys for deterministic output
    - Lazy import (only loads PyYAML when instantiated)
    - Clear error if PyYAML not installed

    Installation:
        pip install kilit[yaml]
        # or
        pip install pyyaml

    Format:
    - Standard YAML format
    - UTF-8 encoding
    - Sorted keys for consistency
    - Block style (not flow style)
    """

    def __init__(self):
        """
        Initialize YAML codec with lazy import.

        Raises:
            KilitFormatError: If PyYAML is not installed
        """
        try:
            import yaml

            self.yaml = yaml
        except ImportError:
            raise KilitFormatError(
                "YAML codec requires PyYAML. Install with: pip install kilit[yaml]"
            )

    @property
    def name(self) -> str:
        return "yaml"

    def loads(self, data: str) -> dict:
        """
        Parse YAML string into dictionary.

        Args:
            data: YAML string

        Returns:
            Parsed dictionary

        Raises:
            KilitFormatError: If YAML is invalid
        """
        try:
            result = self.yaml.safe_load(data)
            if result is None:
                # Empty YAML file
                return {}
            if not isinstance(result, dict):
                raise KilitFormatError(f"Expected YAML mapping (dict), got {type(result).__name__}")
            return result
        except self.yaml.YAMLError as e:
            raise KilitFormatError(f"Invalid YAML: {e}")
        except Exception as e:
            raise KilitFormatError(f"Failed to parse YAML: {e}")

    def dumps(self, data: dict) -> str:
        """
        Format dictionary into YAML string.

        Args:
            data: Dictionary to format

        Returns:
            Pretty-printed YAML string

        Raises:
            KilitFormatError: If formatting fails
        """
        try:
            return self.yaml.safe_dump(
                data, default_flow_style=False, sort_keys=True, allow_unicode=True
            )
        except Exception as e:
            raise KilitFormatError(f"Failed to format YAML: {e}")

    @property
    def default_extension(self) -> str:
        return ".yml"


class CodecRegistry:
    """
    Registry for codec instances with extension-based detection.

    Features:
    - Maps file extensions to codec instances
    - Handles compound extensions (e.g., .json.enc, .yaml.enc)
    - Provides fallback to default codec (JSON)
    - Supports explicit codec selection by name

    Extension Detection:
    - Uses pathlib.Path.suffixes to handle compound extensions
    - Example: "credentials.json.enc" -> suffixes=[".json", ".enc"]
    - Checks for .enc suffix first, then checks remaining extension
    - Falls back to JSON if no match found

    Registered Codecs:
    - JSON: .json, .json.enc
    - YAML: .yml, .yaml, .yml.enc, .yaml.enc (if PyYAML installed)
    """

    def __init__(self):
        """Initialize registry with default codecs."""
        self._codecs = {}
        self._extension_map = {}

        # Register JSON codec (always available)
        json_codec = JSONCodec()
        self.register(json_codec, [".json", ".json.enc"])

        # Try to register YAML codec (optional)
        try:
            yaml_codec = YAMLCodec()
            self.register(yaml_codec, [".yml", ".yaml", ".yml.enc", ".yaml.enc"])
        except KilitFormatError:
            # PyYAML not installed, skip YAML codec
            pass

    def register(self, codec: Codec, extensions: list[str]) -> None:
        """
        Register a codec with file extensions.

        Args:
            codec: Codec instance
            extensions: List of file extensions (e.g., [".json", ".json.enc"])
        """
        self._codecs[codec.name] = codec
        for ext in extensions:
            self._extension_map[ext.lower()] = codec

    def get_codec_for_path(self, path: Path | str) -> Codec:
        """
        Get codec for file path based on extension.

        Args:
            path: File path

        Returns:
            Codec instance

        Detection Logic:
        1. Get all suffixes from path (e.g., [".json", ".enc"])
        2. Check if last suffix is .enc
        3. If .enc, check second-to-last suffix for format
        4. If not .enc, check last suffix for format
        5. Fall back to JSON if no match

        Examples:
        - "credentials.json" -> JSONCodec
        - "credentials.json.enc" -> JSONCodec
        - "credentials.yaml.enc" -> YAMLCodec
        - "credentials.yml" -> YAMLCodec
        - "credentials.enc" -> JSONCodec (fallback)
        - "credentials.txt" -> JSONCodec (fallback)
        """
        path = Path(path)
        suffixes = path.suffixes  # e.g., [".json", ".enc"]

        # Check for compound extension (e.g., .json.enc)
        if len(suffixes) >= 2 and suffixes[-1].lower() == ".enc":
            # Check second-to-last suffix
            format_ext = suffixes[-2].lower()
            if format_ext in self._extension_map:
                return self._extension_map[format_ext]

        # Check last suffix
        if suffixes:
            last_ext = suffixes[-1].lower()
            if last_ext in self._extension_map:
                return self._extension_map[last_ext]

        # Fall back to JSON
        return self._codecs["json"]

    def get_codec_by_name(self, name: str) -> Codec:
        """
        Get codec by name.

        Args:
            name: Codec name (e.g., "json", "yaml")

        Returns:
            Codec instance

        Raises:
            KilitFormatError: If codec not found
        """
        name = name.lower()
        if name not in self._codecs:
            available = ", ".join(self._codecs.keys())
            raise KilitFormatError(f"Unknown codec '{name}'. Available codecs: {available}")
        return self._codecs[name]

    def list_codecs(self) -> list[str]:
        """Return list of available codec names."""
        return list(self._codecs.keys())
