import json
import os
from collections.abc import Sequence
from pathlib import Path
from typing import Any, cast

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow

from home_agent.contracts.protocols import TokenStore


class GoogleAuthManager:
    def __init__(self, store: TokenStore, token_name: str = "google") -> None:
        self.store: TokenStore = store
        self.token_name: str = token_name

    def init_desktop_auth(self, scopes: Sequence[str]) -> Credentials:
        flow = InstalledAppFlow.from_client_config(
            self._load_client_config(),
            scopes=list(scopes),
        )
        try:
            creds_any = flow.run_local_server(port=0, include_granted_scopes="true")
        except AttributeError as exc:
            msg = (
                "google auth browser callback was not received. "
                "Retry `home-agent auth google --init` and complete the consent flow "
                "in the same browser session."
            )
            raise RuntimeError(msg) from exc
        self.store.save(self.token_name, self._creds_to_payload(creds_any))
        return creds_any

    def get_credentials(self, scopes: Sequence[str]) -> Credentials:
        payload = self.store.load(self.token_name)
        if payload is None:
            msg = "google token not initialized; run `home-agent auth google --init` first"
            raise RuntimeError(msg)
        missing_scopes = _find_missing_scopes(payload, scopes)
        if missing_scopes:
            missing_display = ", ".join(missing_scopes)
            msg = (
                "google token is missing required scopes: "
                f"{missing_display}. Run `home-agent auth google --reset` and "
                "`home-agent auth google --init` to grant updated access."
            )
            raise RuntimeError(msg)

        credentials_cls: Any = Credentials
        creds_any = credentials_cls.from_authorized_user_info(payload, scopes=list(scopes))
        creds = cast("Credentials", creds_any)
        if creds.expired and creds.refresh_token:
            creds.refresh(Request())
            self.store.save(self.token_name, self._creds_to_payload(creds))
        return creds

    @staticmethod
    def _creds_to_payload(creds: Credentials) -> dict[str, Any]:
        creds_any: Any = creds
        raw_json = creds_any.to_json()
        parsed = json.loads(raw_json)
        if isinstance(parsed, dict):
            return {str(k): v for k, v in parsed.items()}
        msg = "unexpected google credential payload"
        raise RuntimeError(msg)

    @staticmethod
    def _load_client_config() -> dict[str, Any]:
        client_id = os.environ.get("GOOGLE_CLIENT_ID")
        client_secret = os.environ.get("GOOGLE_CLIENT_SECRET")
        if client_id and client_secret:
            return {
                "installed": {
                    "client_id": client_id,
                    "client_secret": client_secret,
                    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
                    "token_uri": "https://oauth2.googleapis.com/token",
                    "redirect_uris": ["http://localhost"],
                }
            }

        secret_files = sorted(Path.cwd().glob("client_secret_*.json"))
        if len(secret_files) == 1:
            payload = json.loads(secret_files[0].read_text(encoding="utf-8"))
            if isinstance(payload, dict):
                return {str(k): v for k, v in payload.items()}
            msg = f"invalid Google client secrets payload: {secret_files[0].name}"
            raise RuntimeError(msg)
        if len(secret_files) > 1:
            names = ", ".join(path.name for path in secret_files)
            msg = (
                "multiple Google client secrets files found in the current directory: "
                f"{names}. Keep one `client_secret_*.json` file or set "
                "GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET."
            )
            raise RuntimeError(msg)
        msg = (
            "missing GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET; alternatively place one "
            "`client_secret_*.json` file in the current directory"
        )
        raise RuntimeError(msg)


def _find_missing_scopes(payload: dict[str, Any], required_scopes: Sequence[str]) -> list[str]:
    stored_scopes = _extract_stored_scopes(payload)
    if stored_scopes is None:
        return []
    return [scope for scope in required_scopes if scope not in stored_scopes]


def _extract_stored_scopes(payload: dict[str, Any]) -> set[str] | None:
    scopes_value = payload.get("scopes")
    if isinstance(scopes_value, list):
        scopes = {scope for scope in scopes_value if isinstance(scope, str) and scope}
        return scopes or None

    scope_value = payload.get("scope")
    if isinstance(scope_value, str):
        scopes = {scope for scope in scope_value.split() if scope}
        return scopes or None

    return None
