from __future__ import annotations

import json
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path

try:
    import keyring
except ImportError:
    keyring = None

from .config import get_profiles_path, normalize_base_url
from .json_types import JSONObject, KeyringBackend


KEYRING_SERVICE_NAME = "qingflow-mcp"


def _utcnow() -> str:
    return datetime.now(timezone.utc).isoformat()


@dataclass(slots=True)
class SessionProfile:
    profile: str
    base_url: str
    qf_version: str | None
    qf_version_source: str | None
    uid: int
    email: str | None
    nick_name: str | None
    selected_ws_id: int | None
    selected_ws_name: str | None
    persisted: bool
    created_at: str
    updated_at: str

    @classmethod
    def from_dict(cls, value: JSONObject) -> "SessionProfile":
        return cls(
            profile=value["profile"],
            base_url=value["base_url"],
            qf_version=value.get("qf_version"),
            qf_version_source=value.get("qf_version_source"),
            uid=value["uid"],
            email=value.get("email"),
            nick_name=value.get("nick_name"),
            selected_ws_id=value.get("selected_ws_id"),
            selected_ws_name=value.get("selected_ws_name"),
            persisted=bool(value.get("persisted", False)),
            created_at=value.get("created_at", _utcnow()),
            updated_at=value.get("updated_at", _utcnow()),
        )


@dataclass(slots=True)
class BackendSession:
    token: str
    login_token: str | None
    profile: str
    base_url: str
    qf_version: str | None
    qf_version_source: str | None = None


class SessionStore:
    def __init__(self, base_dir: Path | None = None, keyring_backend: KeyringBackend | None = None) -> None:
        profiles_path = get_profiles_path() if base_dir is None else Path(base_dir) / "profiles.json"
        self._profiles_path = profiles_path
        self._profiles_path.parent.mkdir(parents=True, exist_ok=True)
        self._keyring = keyring_backend if keyring_backend is not None else keyring
        self._memory_sessions: dict[str, BackendSession] = {}
        self._logged_out_profiles: set[str] = set()

    def save_session(
        self,
        *,
        profile: str,
        base_url: str,
        qf_version: str | None,
        qf_version_source: str | None = None,
        token: str,
        login_token: str | None,
        uid: int,
        email: str | None,
        nick_name: str | None,
        persist: bool,
    ) -> SessionProfile:
        now = _utcnow()
        previous = self.get_profile(profile)
        persisted = False
        if persist:
            persisted = self._set_secret(self._token_key(profile), token)
            if login_token:
                self._set_secret(self._login_token_key(profile), login_token)
            else:
                self._delete_secret(self._login_token_key(profile))
        else:
            self._delete_secret(self._token_key(profile))
            self._delete_secret(self._login_token_key(profile))
        session_profile = SessionProfile(
            profile=profile,
            base_url=normalize_base_url(base_url) or base_url,
            qf_version=(str(qf_version).strip() or None) if qf_version is not None else None,
            qf_version_source=(str(qf_version_source).strip() or None) if qf_version_source is not None else None,
            uid=uid,
            email=email,
            nick_name=nick_name,
            selected_ws_id=None,
            selected_ws_name=None,
            persisted=persisted,
            created_at=previous.created_at if previous else now,
            updated_at=now,
        )
        self._memory_sessions[profile] = BackendSession(
            token=token,
            login_token=login_token,
            profile=profile,
            base_url=session_profile.base_url,
            qf_version=session_profile.qf_version,
            qf_version_source=session_profile.qf_version_source,
        )
        self._logged_out_profiles.discard(profile)
        self._upsert_profile(session_profile)
        return session_profile

    def get_profile(self, profile: str) -> SessionProfile | None:
        payload = self._load_profiles()
        raw_profile = payload.get("profiles", {}).get(profile)
        if not raw_profile:
            return None
        return SessionProfile.from_dict(raw_profile)

    def get_backend_session(self, profile: str) -> BackendSession | None:
        if profile in self._logged_out_profiles:
            return None
        memory_session = self._memory_sessions.get(profile)
        if memory_session:
            return memory_session
        session_profile = self.get_profile(profile)
        if not session_profile or not session_profile.persisted:
            return None
        token = self._get_secret(self._token_key(profile))
        if not token:
            return None
        backend_session = BackendSession(
            token=token,
            login_token=self._get_secret(self._login_token_key(profile)),
            profile=profile,
            base_url=session_profile.base_url,
            qf_version=session_profile.qf_version,
            qf_version_source=session_profile.qf_version_source,
        )
        self._memory_sessions[profile] = backend_session
        return backend_session

    def select_workspace(self, profile: str, ws_id: int, ws_name: str | None) -> SessionProfile:
        session_profile = self.get_profile(profile)
        if session_profile is None:
            raise KeyError(profile)
        session_profile.selected_ws_id = ws_id
        session_profile.selected_ws_name = ws_name
        session_profile.updated_at = _utcnow()
        self._upsert_profile(session_profile)
        return session_profile

    def update_route(self, profile: str, *, qf_version: str | None, qf_version_source: str | None) -> SessionProfile:
        session_profile = self.get_profile(profile)
        if session_profile is None:
            raise KeyError(profile)
        session_profile.qf_version = (str(qf_version).strip() or None) if qf_version is not None else None
        session_profile.qf_version_source = (str(qf_version_source).strip() or None) if qf_version_source is not None else None
        session_profile.updated_at = _utcnow()
        self._upsert_profile(session_profile)
        backend_session = self._memory_sessions.get(profile)
        if backend_session is not None:
            backend_session.qf_version = session_profile.qf_version
            backend_session.qf_version_source = session_profile.qf_version_source
        return session_profile

    def logout(self, profile: str, forget_persisted: bool = False) -> None:
        self._memory_sessions.pop(profile, None)
        if forget_persisted:
            self.invalidate(profile)
            return
        if self.get_profile(profile):
            self._logged_out_profiles.add(profile)

    def invalidate(self, profile: str) -> None:
        self._memory_sessions.pop(profile, None)
        self._logged_out_profiles.discard(profile)
        self._delete_secret(self._token_key(profile))
        self._delete_secret(self._login_token_key(profile))
        payload = self._load_profiles()
        profiles = payload.get("profiles", {})
        if profile in profiles:
            profiles.pop(profile)
            self._save_profiles(payload)

    def has_profile(self, profile: str) -> bool:
        return self.get_profile(profile) is not None

    def _token_key(self, profile: str) -> str:
        return f"{profile}:token"

    def _login_token_key(self, profile: str) -> str:
        return f"{profile}:login-token"

    def _upsert_profile(self, profile: SessionProfile) -> None:
        payload = self._load_profiles()
        payload.setdefault("profiles", {})[profile.profile] = asdict(profile)
        self._save_profiles(payload)

    def _load_profiles(self) -> JSONObject:
        if not self._profiles_path.exists():
            return {"profiles": {}}
        with self._profiles_path.open("r", encoding="utf-8") as handle:
            return json.load(handle)

    def _save_profiles(self, payload: JSONObject) -> None:
        self._profiles_path.parent.mkdir(parents=True, exist_ok=True)
        with self._profiles_path.open("w", encoding="utf-8") as handle:
            json.dump(payload, handle, ensure_ascii=False, indent=2)

    def _set_secret(self, key: str, value: str) -> bool:
        if self._keyring is None:
            return False
        try:
            self._keyring.set_password(KEYRING_SERVICE_NAME, key, value)
            return True
        except Exception:
            return False

    def _get_secret(self, key: str) -> str | None:
        if self._keyring is None:
            return None
        try:
            return self._keyring.get_password(KEYRING_SERVICE_NAME, key)
        except Exception:
            return None

    def _delete_secret(self, key: str) -> None:
        if self._keyring is None:
            return
        try:
            self._keyring.delete_password(KEYRING_SERVICE_NAME, key)
        except Exception:
            return
