"""
Google 接入状态与 OAuth 流程。

设计目标：
- 保持文件式单用户实现
- 兼容已有 token.json 直连模式
- 以最小改动补齐状态接口、授权向导和首次同步
"""

from __future__ import annotations

import json
import os
import secrets
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Iterator
from urllib.parse import urlencode

try:
    import requests
except ImportError:  # pragma: no cover - handled lazily for local mode
    requests = None

from config.settings import (
    GCAL_CALENDAR_ID,
    GCAL_MAX_RETRIES,
    GCAL_SSL_VERIFY,
    GCAL_TIMEOUT,
    GCAL_TOKEN_PATH,
    GOOGLE_OAUTH_SESSION_TTL_MINUTES,
    GOOGLE_STATE_FILE,
    PROXIES,
    TIMEZONE,
)
from gcal.client import CalendarAuthError, CalendarClient
from services.local_event_store import list_syncable_records, update_sync_status

try:
    import fcntl
except ImportError:  # pragma: no cover - Windows fallback
    fcntl = None

_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
_TOKEN_URL = "https://oauth2.googleapis.com/token"
_SCOPES = ["https://www.googleapis.com/auth/calendar"]


def _default_sync_summary() -> dict:
    return {
        "status": "idle",
        "total": 0,
        "succeeded": 0,
        "failed": 0,
        "skipped": 0,
        "started_at": "",
        "finished_at": "",
        "last_error": "",
    }


def _has_valid_token_file() -> bool:
    try:
        with open(GCAL_TOKEN_PATH, encoding="utf-8") as f:
            data = json.load(f)
    except Exception:
        return False
    required = {"client_id", "client_secret", "refresh_token", "token_uri"}
    return required.issubset(data.keys())


def _load_token_data() -> dict:
    with open(GCAL_TOKEN_PATH, encoding="utf-8") as f:
        return json.load(f)


def _default_state() -> dict:
    if _has_valid_token_file():
        return {
            "active_mode": "google",
            "google_state": "ready",
            "last_error": "",
            "last_sync_summary": _default_sync_summary(),
            "last_sync_time": "",
            "oauth_session": {},
        }
    return {
        "active_mode": "local",
        "google_state": "not_connected",
        "last_error": "",
        "last_sync_summary": _default_sync_summary(),
        "last_sync_time": "",
        "oauth_session": {},
    }


def _atomic_write_json(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp_name = None
    try:
        with NamedTemporaryFile(
            "w",
            dir=path.parent,
            delete=False,
            encoding="utf-8",
        ) as tmp:
            json.dump(data, tmp, indent=2, ensure_ascii=False)
            tmp.flush()
            os.fsync(tmp.fileno())
            tmp_name = tmp.name
        os.replace(tmp_name, path)
    finally:
        if tmp_name and os.path.exists(tmp_name):
            os.unlink(tmp_name)


def _read_state(path: Path) -> dict:
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        return _default_state()
    if not isinstance(data, dict):
        return _default_state()
    return data


def _normalize_state(data: dict) -> dict:
    state = dict(_default_state())
    state.update(data or {})
    state.setdefault("last_sync_summary", _default_sync_summary())
    state.setdefault("oauth_session", {})
    sync_summary = dict(_default_sync_summary())
    sync_summary.update(state.get("last_sync_summary") or {})
    state["last_sync_summary"] = sync_summary

    session = dict(state.get("oauth_session") or {})
    expires_at = session.get("expires_at", "")
    if expires_at:
        try:
            expired = datetime.fromisoformat(expires_at) <= datetime.now(timezone.utc)
        except ValueError:
            expired = True
        if expired:
            session = {}
            if state.get("google_state") == "authorizing":
                state["google_state"] = "ready" if _has_valid_token_file() else "not_connected"
    state["oauth_session"] = session

    if not GOOGLE_STATE_FILE.exists() and _has_valid_token_file():
        state["active_mode"] = "google"
        state["google_state"] = "ready"

    if state.get("active_mode") == "google" and not _has_valid_token_file():
        state["active_mode"] = "local"
        if state.get("google_state") == "ready":
            state["google_state"] = "not_connected"

    if state.get("google_state") == "not_connected" and _has_valid_token_file():
        state["google_state"] = "ready"
        if state.get("active_mode") == "local" and not state.get("last_sync_time"):
            state["active_mode"] = "google"

    return state


@contextmanager
def _locked_state(path: str | Path | None = None) -> Iterator[tuple[Path, dict]]:
    state_path = Path(path) if path is not None else GOOGLE_STATE_FILE
    state_path.parent.mkdir(parents=True, exist_ok=True)
    lock_path = state_path.with_suffix(state_path.suffix + ".lock")
    with open(lock_path, "a+", encoding="utf-8") as lock_file:
        if fcntl is not None:
            fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
        yield state_path, _normalize_state(_read_state(state_path))


def load_state(path: str | Path | None = None) -> dict:
    with _locked_state(path) as (_, data):
        return data


def save_state(data: dict, path: str | Path | None = None) -> dict:
    with _locked_state(path) as (state_path, _):
        normalized = _normalize_state(data)
        _atomic_write_json(state_path, normalized)
        return normalized


def has_token() -> bool:
    return _has_valid_token_file()


def get_active_mode() -> str:
    return load_state().get("active_mode", "local")


def mask_client_id(client_id: str) -> str:
    if not client_id:
        return ""
    if len(client_id) <= 10:
        return client_id[:2] + "***"
    return f"{client_id[:4]}***{client_id[-4:]}"


def _current_step(state: dict) -> str:
    google_state = state.get("google_state", "not_connected")
    if google_state in {"not_connected", "error"}:
        return "credentials"
    if google_state == "authorizing":
        return "authorize"
    if google_state in {"connected_pending_sync", "syncing"}:
        return "sync"
    return "done"


def get_status_payload() -> dict:
    state = load_state()
    token_client_id = ""
    if has_token():
        try:
            token_client_id = _load_token_data().get("client_id", "")
        except Exception:
            token_client_id = ""
    pending_session = state.get("oauth_session") or {}
    client_id = pending_session.get("client_id") or token_client_id
    return {
        "active_mode": state.get("active_mode", "local"),
        "google_state": state.get("google_state", "not_connected"),
        "has_token": has_token(),
        "masked_client_id": mask_client_id(client_id),
        "needs_initial_sync": state.get("active_mode") != "google" and has_token(),
        "current_step": _current_step(state),
        "last_error": state.get("last_error", ""),
        "last_sync_summary": state.get("last_sync_summary", _default_sync_summary()),
        "last_sync_time": state.get("last_sync_time", ""),
    }


def get_guide_payload(callback_url: str) -> dict:
    status = get_status_payload()
    return {
        "current_step": status["current_step"],
        "active_mode": status["active_mode"],
        "google_state": status["google_state"],
        "callback_url": callback_url,
        "required_fields": ["client_id", "client_secret"],
        "steps": [
            {
                "key": "credentials",
                "title": "填写 Google OAuth 凭据",
                "description": "用户提供自己的 Google Client ID 和 Client Secret。",
            },
            {
                "key": "authorize",
                "title": "跳转 Google 授权",
                "description": "后端生成授权链接，用户在浏览器完成授权。",
            },
            {
                "key": "sync",
                "title": "同步本地事件",
                "description": "将本地模式下的事件补推到 Google Calendar。",
            },
            {
                "key": "done",
                "title": "切换到 Google 模式",
                "description": "首次同步成功后，系统正式改用 Google 作为事件源。",
            },
        ],
    }


def _build_authorize_url(client_id: str, redirect_uri: str, state_token: str) -> str:
    params = {
        "client_id": client_id,
        "redirect_uri": redirect_uri,
        "response_type": "code",
        "scope": " ".join(_SCOPES),
        "access_type": "offline",
        "prompt": "consent",
        "state": state_token,
    }
    return f"{_AUTH_URL}?{urlencode(params)}"


def start_oauth_session(client_id: str, client_secret: str, redirect_uri: str) -> dict:
    state_token = secrets.token_urlsafe(24)
    expires_at = datetime.now(timezone.utc) + timedelta(
        minutes=GOOGLE_OAUTH_SESSION_TTL_MINUTES
    )
    with _locked_state() as (state_path, data):
        data["google_state"] = "authorizing"
        data["active_mode"] = "local"
        data["last_error"] = ""
        data["oauth_session"] = {
            "state": state_token,
            "client_id": client_id,
            "client_secret": client_secret,
            "redirect_uri": redirect_uri,
            "status": "pending",
            "created_at": datetime.now(timezone.utc).isoformat(),
            "expires_at": expires_at.isoformat(),
        }
        _atomic_write_json(state_path, _normalize_state(data))

    return {
        "ok": True,
        "authorize_url": _build_authorize_url(client_id, redirect_uri, state_token),
        "expires_at": expires_at.isoformat(),
        "google_state": "authorizing",
    }


def _write_token_file(token_data: dict) -> None:
    GCAL_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
    _atomic_write_json(GCAL_TOKEN_PATH, token_data)


def complete_oauth_callback(
    state_token: str,
    code: str | None,
    error: str | None,
    error_description: str | None,
) -> dict:
    with _locked_state() as (state_path, data):
        session = dict(data.get("oauth_session") or {})
        if not session:
            data["google_state"] = "error"
            data["last_error"] = "未找到进行中的授权会话"
            _atomic_write_json(state_path, _normalize_state(data))
            raise CalendarAuthError(data["last_error"])

        if state_token != session.get("state"):
            data["google_state"] = "error"
            data["last_error"] = "OAuth state 不匹配"
            _atomic_write_json(state_path, _normalize_state(data))
            raise CalendarAuthError(data["last_error"])

        if error:
            message = error_description or error
            data["google_state"] = "error"
            data["last_error"] = message
            data["oauth_session"] = {}
            _atomic_write_json(state_path, _normalize_state(data))
            raise CalendarAuthError(message)

        if not code:
            data["google_state"] = "error"
            data["last_error"] = "授权回调缺少 code"
            data["oauth_session"] = {}
            _atomic_write_json(state_path, _normalize_state(data))
            raise CalendarAuthError(data["last_error"])

        redirect_uri = session.get("redirect_uri", "")
        req = _require_requests()
        resp = req.post(
            _TOKEN_URL,
            data={
                "code": code,
                "client_id": session.get("client_id", ""),
                "client_secret": session.get("client_secret", ""),
                "redirect_uri": redirect_uri,
                "grant_type": "authorization_code",
            },
            proxies=PROXIES,
            verify=GCAL_SSL_VERIFY,
            timeout=GCAL_TIMEOUT,
        )
        if resp.status_code != 200:
            data["google_state"] = "error"
            data["last_error"] = f"Token 交换失败 {resp.status_code}: {resp.text[:200]}"
            data["oauth_session"] = {}
            _atomic_write_json(state_path, _normalize_state(data))
            raise CalendarAuthError(data["last_error"])

        token_resp = resp.json()
        refresh_token = token_resp.get("refresh_token")
        access_token = token_resp.get("access_token", "")
        if not refresh_token:
            data["google_state"] = "error"
            data["last_error"] = "Google 未返回 refresh_token，请确认使用 consent 授权"
            data["oauth_session"] = {}
            _atomic_write_json(state_path, _normalize_state(data))
            raise CalendarAuthError(data["last_error"])

        _write_token_file({
            "client_id": session.get("client_id", ""),
            "client_secret": session.get("client_secret", ""),
            "refresh_token": refresh_token,
            "token": access_token,
            "token_uri": _TOKEN_URL,
        })

        data["active_mode"] = "local"
        data["google_state"] = "connected_pending_sync"
        data["last_error"] = ""
        data["oauth_session"] = {}
        _atomic_write_json(state_path, _normalize_state(data))
        return _normalize_state(data)


def get_last_sync_status() -> dict:
    return load_state().get("last_sync_summary", _default_sync_summary())


def sync_local_events_to_google() -> dict:
    if not has_token():
        raise CalendarAuthError(f"Token 文件不存在: {GCAL_TOKEN_PATH}")

    with _locked_state() as (state_path, data):
        sync_summary = _default_sync_summary()
        sync_summary["status"] = "running"
        sync_summary["started_at"] = datetime.now(timezone.utc).isoformat()
        data["google_state"] = "syncing"
        data["last_error"] = ""
        data["last_sync_summary"] = sync_summary
        _atomic_write_json(state_path, _normalize_state(data))

    client = CalendarClient(
        token_path=GCAL_TOKEN_PATH,
        proxies=PROXIES,
        calendar_id=GCAL_CALENDAR_ID,
        timeout=GCAL_TIMEOUT,
        max_retries=GCAL_MAX_RETRIES,
        verify=GCAL_SSL_VERIFY,
    )

    summary = _default_sync_summary()
    summary["status"] = "finished"
    summary["started_at"] = datetime.now(timezone.utc).isoformat()
    errors: list[str] = []

    try:
        client.list_calendars()
    except Exception as e:
        with _locked_state() as (state_path, data):
            data["active_mode"] = "local"
            data["google_state"] = "error"
            data["last_error"] = str(e)
            summary["status"] = "failed"
            summary["last_error"] = str(e)
            summary["finished_at"] = datetime.now(timezone.utc).isoformat()
            data["last_sync_summary"] = summary
            data["last_sync_time"] = summary["finished_at"]
            _atomic_write_json(state_path, _normalize_state(data))
        raise

    records = list_syncable_records()
    summary["total"] = len(records)
    for record in records:
        try:
            google_event_id = client.create_event(
                summary=record.get("summary", "无标题"),
                start=datetime.fromisoformat(record["start"]),
                end=datetime.fromisoformat(record["end"]),
                location=record.get("location", ""),
                description=record.get("description", ""),
                tz=TIMEZONE,
            )
            update_sync_status(
                record["id"],
                sync_state="synced",
                google_event_id=google_event_id,
                last_sync_error="",
            )
            summary["succeeded"] += 1
        except Exception as e:
            update_sync_status(
                record["id"],
                sync_state="failed",
                google_event_id="",
                last_sync_error=str(e)[:300],
            )
            summary["failed"] += 1
            errors.append(f"{record.get('summary', record['id'])}: {e}")

    summary["finished_at"] = datetime.now(timezone.utc).isoformat()
    summary["last_error"] = errors[0] if errors else ""

    with _locked_state() as (state_path, data):
        data["last_sync_summary"] = summary
        data["last_sync_time"] = summary["finished_at"]
        if summary["failed"] == 0:
            data["active_mode"] = "google"
            data["google_state"] = "ready"
            data["last_error"] = ""
        else:
            data["active_mode"] = "local"
            data["google_state"] = "connected_pending_sync"
            data["last_error"] = summary["last_error"]
        _atomic_write_json(state_path, _normalize_state(data))
    return summary


def _require_requests():
    if requests is None:
        raise CalendarAuthError("缺少 requests 依赖，请先安装 requirements.txt")
    return requests
