"""Cache manager for token storage and retrieval with JSON persistence."""

import csv
from datetime import datetime, timezone
from pathlib import Path

from pydantic import ValidationError as PydanticValidationError

from teams_phone.constants import CSV_STALE_DAYS, DEFAULT_CACHE_PATH, DEFAULT_TOKEN_PATH
from teams_phone.exceptions import ConfigurationError
from teams_phone.models import CachedToken, CacheMetadata


class CacheManager:
    """Manages token caching with JSON persistence.

    Provides methods to save, retrieve, and clear authentication tokens
    for tenants. Tokens are stored as JSON files in the token directory.

    Attributes:
        cache_dir: Path to the cache directory for CSV files.
        token_dir: Path to the token storage directory.
    """

    def __init__(
        self,
        cache_dir: str | None = None,
        token_dir: str | None = None,
    ) -> None:
        """Initialize the CacheManager.

        Args:
            cache_dir: Path to the cache directory. Defaults to
                ~/.teams-phone/cache if not specified.
            token_dir: Path to the token directory. Defaults to
                ~/.teams-phone/tokens if not specified.
        """
        cache_str = cache_dir if cache_dir is not None else DEFAULT_CACHE_PATH
        self._cache_dir: Path = Path(cache_str).expanduser()

        token_str = token_dir if token_dir is not None else DEFAULT_TOKEN_PATH
        self._token_dir: Path = Path(token_str).expanduser()

    @property
    def cache_dir(self) -> Path:
        """Path to the cache directory."""
        return self._cache_dir

    @property
    def token_dir(self) -> Path:
        """Path to the token directory."""
        return self._token_dir

    def ensure_token_dir(self) -> None:
        """Create the token directory if it doesn't exist.

        Creates all parent directories as needed.

        Raises:
            ConfigurationError: If directory creation fails due to permission error.
        """
        try:
            self._token_dir.mkdir(parents=True, exist_ok=True)
        except PermissionError as e:
            raise ConfigurationError(
                f"Cannot create token directory {self._token_dir}: {e}",
                remediation=(
                    f"Check that you have write permissions to {self._token_dir}.\n"
                    "You may need to create the directory manually or run with elevated privileges."
                ),
            ) from e

    def _get_token_path(self, tenant_name: str) -> Path:
        """Get the file path for a tenant's token.

        Args:
            tenant_name: The tenant profile name.

        Returns:
            Path to the token JSON file.
        """
        return self._token_dir / f"{tenant_name}.json"

    def get_token(self, tenant_name: str) -> CachedToken | None:
        """Retrieve a cached token for a tenant.

        Args:
            tenant_name: The tenant profile name.

        Returns:
            CachedToken if found and valid, None if file doesn't exist or
            contains invalid JSON.

        Note:
            Returns None silently for missing files or invalid JSON.
            This enables graceful fallback to re-authentication.
        """
        token_path = self._get_token_path(tenant_name)

        if not token_path.exists():
            return None

        try:
            content = token_path.read_text(encoding="utf-8")
            return CachedToken.model_validate_json(content)
        except (PydanticValidationError, ValueError):
            # Invalid JSON or failed validation - treat as missing
            return None

    def save_token(self, tenant_name: str, token: CachedToken) -> None:
        """Save a token to the cache.

        Creates the token directory if it doesn't exist.

        Args:
            tenant_name: The tenant profile name.
            token: The token to cache.

        Raises:
            ConfigurationError: If the token directory cannot be created
                due to permission issues.
        """
        self.ensure_token_dir()

        token_path = self._get_token_path(tenant_name)
        content = token.model_dump_json()
        token_path.write_text(content, encoding="utf-8")

    def clear_tokens(self, tenant_name: str | None = None) -> None:
        """Clear cached tokens.

        Args:
            tenant_name: The tenant name to clear tokens for.
                If None, clears all token files.

        Note:
            Does not raise errors for missing files or directories.
            The token directory is preserved when clearing all tokens.
        """
        if tenant_name is not None:
            # Clear specific tenant's token
            token_path = self._get_token_path(tenant_name)
            token_path.unlink(missing_ok=True)
        else:
            # Clear all token files
            if self._token_dir.exists():
                for token_file in self._token_dir.glob("*.json"):
                    token_file.unlink(missing_ok=True)

    def _read_csv(self, filename: str) -> list[dict[str, str]]:
        """Read a CSV file from the cache directory.

        Args:
            filename: Name of the CSV file (e.g., "locations.csv").

        Returns:
            List of dictionaries, one per row. Empty list if file
            doesn't exist or is malformed.

        Note:
            Uses UTF-8 with BOM support (utf-8-sig encoding) to handle
            PowerShell exports which may include a byte order mark.
        """
        csv_path = self._cache_dir / filename

        if not csv_path.exists():
            return []

        try:
            with csv_path.open(encoding="utf-8-sig") as f:
                reader = csv.DictReader(f)
                return list(reader)
        except csv.Error:
            return []

    def read_locations_csv(self) -> list[dict[str, str]]:
        """Read emergency locations from the cache.

        Returns:
            List of location dictionaries with keys like locationId,
            description, address, city, etc. Empty list if file doesn't
            exist or is malformed.
        """
        return self._read_csv("locations.csv")

    def read_policies_csv(self) -> list[dict[str, str]]:
        """Read policies from the cache.

        Returns:
            List of policy dictionaries with keys like policyType,
            policyName, policyId, description, isGlobal. Empty list if
            file doesn't exist or is malformed.
        """
        return self._read_csv("policies.csv")

    def get_csv_metadata(self) -> CacheMetadata | None:
        """Get metadata about the CSV cache files.

        Checks for locations.csv and policies.csv, returning metadata
        about when they were last updated and how many records they contain.

        Returns:
            CacheMetadata with last_updated (oldest mtime of the two files),
            locations_count, and policies_count. Returns None if neither
            CSV file exists.
        """
        locations_path = self._cache_dir / "locations.csv"
        policies_path = self._cache_dir / "policies.csv"

        locations_exists = locations_path.exists()
        policies_exists = policies_path.exists()

        if not locations_exists and not policies_exists:
            return None

        # Get modification times, handling potential race conditions
        mtimes: list[datetime] = []

        if locations_exists:
            try:
                mtime = locations_path.stat().st_mtime
                mtimes.append(datetime.fromtimestamp(mtime, tz=timezone.utc))
            except FileNotFoundError:
                pass

        if policies_exists:
            try:
                mtime = policies_path.stat().st_mtime
                mtimes.append(datetime.fromtimestamp(mtime, tz=timezone.utc))
            except FileNotFoundError:
                pass

        # Use the OLDER mtime (indicates when cache was last completely updated)
        last_updated = min(mtimes) if mtimes else None

        # Get row counts using existing methods
        locations_count = len(self.read_locations_csv())
        policies_count = len(self.read_policies_csv())

        return CacheMetadata(
            last_updated=last_updated,
            locations_count=locations_count,
            policies_count=policies_count,
        )

    def get_cache_age_days(self) -> int | None:
        """Get the age of the cache in days.

        Returns:
            Number of days since the oldest CSV file was modified,
            or None if no cache metadata exists or last_updated is None.
        """
        metadata = self.get_csv_metadata()

        if metadata is None or metadata.last_updated is None:
            return None

        now = datetime.now(timezone.utc)
        delta = now - metadata.last_updated
        return delta.days

    def is_cache_stale(self) -> bool:
        """Check if the CSV cache is stale and needs refreshing.

        Returns:
            True if no cache exists or the cache is older than CSV_STALE_DAYS
            (30 days). False if the cache is fresh.
        """
        age_days = self.get_cache_age_days()

        if age_days is None:
            return True

        return age_days > CSV_STALE_DAYS
