import json
import os
import stat
import time
from pathlib import Path
from typing import Any

from filelock import FileLock

from otto.config import OTTO_HOME
from otto.log import get_logger

# === Constants ===

AUTH_FILE: Path = OTTO_HOME / "config" / "auth.json"
_LOCK_TIMEOUT = 10
_EXPIRY_BUFFER_SECONDS = 300

log = get_logger(__name__)


# === Helpers ===


def _mask(value: str) -> str:
    if len(value) <= 8:
        return "***"
    return f"{value[:4]}...{value[-4:]}"


# === Storage ===


class AuthStorage:
    """Thread/process-safe credential storage backed by auth.json."""

    def __init__(self, path: Path | None = None) -> None:
        self._path = path or AUTH_FILE
        self._lock = FileLock(str(self._path) + ".lock", timeout=_LOCK_TIMEOUT)

    # === Low-level I/O ===

    def _read(self) -> dict[str, Any]:
        if not self._path.exists():
            return {"credentials": {}}
        try:
            data = json.loads(self._path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            log.warning("Corrupt auth storage; starting fresh", path=str(self._path))
            return {"credentials": {}}

        if not isinstance(data, dict):
            return {"credentials": {}}
        credentials = data.get("credentials")
        if not isinstance(credentials, dict):
            data["credentials"] = {}
        return data

    def _write(self, data: dict[str, Any]) -> None:
        self._path.parent.mkdir(parents=True, exist_ok=True)
        self._path.write_text(json.dumps(data, indent=2), encoding="utf-8")
        os.chmod(self._path, stat.S_IRUSR | stat.S_IWUSR)

    @staticmethod
    def _is_expired(expires: int | float | None) -> bool:
        if expires is None:
            return True
        return time.time() >= (float(expires) - _EXPIRY_BUFFER_SECONDS)

    @staticmethod
    def _provider_api_key(creds: dict[str, Any]) -> str | None:
        access = creds.get("access")
        if isinstance(access, str) and access:
            return access
        key = creds.get("key")
        if isinstance(key, str) and key:
            return key
        return None

    # === Credential CRUD ===

    def get_credentials(self, provider: str) -> dict[str, Any] | None:
        with self._lock:
            creds = self._read()["credentials"].get(provider)
            return creds if isinstance(creds, dict) else None

    def save_credentials(self, provider: str, creds: dict[str, Any]) -> None:
        with self._lock:
            data = self._read()
            data["credentials"][provider] = dict(creds)
            self._write(data)

        masked_access = None
        access = creds.get("access")
        if isinstance(access, str):
            masked_access = _mask(access)
        log.debug(
            "Saved credentials",
            provider=provider,
            type=creds.get("type"),
            access=masked_access,
        )

    def delete_credentials(self, provider: str) -> None:
        with self._lock:
            data = self._read()
            data["credentials"].pop(provider, None)
            self._write(data)

    # === API Key Resolution ===

    async def get_api_key(self, provider: str) -> str | None:
        creds = self.get_credentials(provider)
        if creds is None:
            return None

        cred_type = creds.get("type", "api_key")
        if cred_type == "api_key":
            return self._provider_api_key(creds)
        if cred_type != "oauth":
            log.warning("Unknown credential type", provider=provider, type=cred_type)
            return None

        expires = creds.get("expires")
        if isinstance(expires, str):
            try:
                expires = float(expires)
            except ValueError:
                expires = None

        if not self._is_expired(expires if isinstance(expires, (int, float)) else None):
            return self._provider_api_key(creds)
        return await self._refresh_and_return(provider, creds)

    async def _refresh_and_return(self, provider: str, creds: dict[str, Any]) -> str | None:
        with self._lock:
            fresh = self._read()["credentials"].get(provider)
            if isinstance(fresh, dict):
                fresh_expires = fresh.get("expires")
                if isinstance(fresh_expires, str):
                    try:
                        fresh_expires = float(fresh_expires)
                    except ValueError:
                        fresh_expires = None
                if not self._is_expired(
                    fresh_expires if isinstance(fresh_expires, (int, float)) else None
                ):
                    return self._provider_api_key(fresh)
                creds = fresh

            updated = await self._do_refresh(provider, creds)
            if not updated:
                return None

            data = self._read()
            data["credentials"][provider] = updated
            self._write(data)
            return self._provider_api_key(updated)

    async def _do_refresh(self, provider: str, creds: dict[str, Any]) -> dict[str, Any] | None:
        """Dispatch to the correct provider refresh function."""
        refresh = creds.get("refresh")
        if not refresh:
            log.warning("No refresh token for provider", provider=provider)
            return None

        try:
            if provider == "anthropic":
                from otto.auth.anthropic import refresh_token

                return await refresh_token(refresh)
            if provider == "openai":
                from otto.auth.openai import refresh_token

                return await refresh_token(refresh)
            if provider == "google":
                from otto.auth.google import refresh_token

                project_id = creds.get("projectId")
                return await refresh_token(refresh, project_id)
        except Exception:
            log.exception("Token refresh failed", provider=provider)
            return None

        log.warning("No refresh handler for provider", provider=provider)
        return None
