"""
Google Calendar API 统一访问层。

第三方依赖：
  - Google Calendar API v3（REST）
  - HTTP 代理：由 proxies 参数注入（默认读 config/settings.py）
  - Token 文件：gcalcli 兼容格式
    字段：client_id / client_secret / refresh_token / token_uri

所有方法返回结构化的 CalendarEvent 对象，上层不再接触原始 API 数据。
"""

from __future__ import annotations

import json
import logging
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

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

from .models import CalendarEvent

logger = logging.getLogger(__name__)

_EVENTS_URL    = "https://www.googleapis.com/calendar/v3/calendars/{cal_id}/events"
_CAL_LIST_URL  = "https://www.googleapis.com/calendar/v3/users/me/calendarList"
_TOKEN_REFRESH = "https://oauth2.googleapis.com/token"


class CalendarAuthError(Exception):
    """Token 鉴权相关错误。"""


class CalendarAPIError(Exception):
    """Google Calendar API 请求错误。"""


class CalendarClient:
    """
    Google Calendar 访问客户端。

    用法示例：
        from config.settings import GCAL_TOKEN_PATH, PROXIES
        from gcal.client import CalendarClient
        from services.time_window import get_today_window

        client = CalendarClient(GCAL_TOKEN_PATH, PROXIES)
        start, end = get_today_window()
        events = client.list_events(start, end)

    Token 过期时自动刷新并写回文件，无需手动管理。
    """

    def __init__(
        self,
        token_path: str | Path,
        proxies: dict[str, str] | None = None,
        calendar_id: str = "primary",
        timeout: int = 10,
        max_retries: int = 3,
        verify: bool | str = True,
    ):
        self.token_path  = Path(token_path)
        self.proxies     = proxies or {}
        self.calendar_id = calendar_id
        self.timeout     = timeout
        self.max_retries = max_retries
        self.verify      = verify
        self._access_token: str | None = None

    # ──────────────────────────── Token ───────────────────────────

    def _load_token_file(self) -> dict:
        try:
            with open(self.token_path) as f:
                return json.load(f)
        except FileNotFoundError:
            raise CalendarAuthError(f"Token 文件不存在: {self.token_path}")
        except json.JSONDecodeError as e:
            raise CalendarAuthError(f"Token 文件格式错误: {e}")

    def refresh_token(self) -> str:
        """刷新 access_token 并写回文件，返回新 token。"""
        req = _require_requests()
        data = self._load_token_file()
        resp = req.post(
            data.get("token_uri", _TOKEN_REFRESH),
            data={
                "client_id":     data["client_id"],
                "client_secret": data["client_secret"],
                "refresh_token": data["refresh_token"],
                "grant_type":    "refresh_token",
            },
            proxies=self.proxies,
            verify=self.verify,
            timeout=self.timeout,
        )
        if resp.status_code != 200:
            raise CalendarAuthError(
                f"Token 刷新失败 {resp.status_code}: {resp.text[:200]}"
            )
        new_token = resp.json()["access_token"]
        data["token"] = new_token
        with open(self.token_path, "w") as f:
            json.dump(data, f, indent=2)
        self._access_token = new_token
        logger.debug("Token 刷新成功")
        return new_token

    def _get_token(self) -> str:
        if not self._access_token:
            self.refresh_token()
        return self._access_token  # type: ignore[return-value]

    def _auth_headers(self) -> dict[str, str]:
        return {"Authorization": f"Bearer {self._get_token()}"}

    # ──────────────────────────── HTTP ────────────────────────────

    def _get(self, url: str, params: dict | None = None) -> dict:
        """GET 请求，带 token 自动刷新和指数退避重试。"""
        req = _require_requests()
        refreshed = False
        for attempt in range(self.max_retries):
            try:
                resp = req.get(
                    url,
                    headers=self._auth_headers(),
                    params=params,
                    proxies=self.proxies,
                    verify=self.verify,
                    timeout=self.timeout,
                )
                if resp.status_code == 200:
                    return resp.json()
                if resp.status_code == 401 and not refreshed:
                    logger.info("Token 已过期，自动刷新…")
                    self.refresh_token()
                    refreshed = True
                    continue
                if resp.status_code >= 500 and attempt < self.max_retries - 1:
                    time.sleep(2 ** attempt)
                    continue
                raise CalendarAPIError(
                    f"API 错误 {resp.status_code}: {resp.text[:200]}"
                )
            except (req.ConnectionError, req.Timeout) as e:
                if attempt < self.max_retries - 1:
                    time.sleep(2 ** attempt)
                    continue
                raise CalendarAPIError(f"网络请求失败: {e}") from e
        raise CalendarAPIError("所有重试均失败")

    def _post(self, url: str, payload: dict) -> dict:
        req = _require_requests()
        resp = req.post(
            url,
            headers={**self._auth_headers(), "Content-Type": "application/json"},
            json=payload,
            proxies=self.proxies,
            verify=self.verify,
            timeout=self.timeout,
        )
        if resp.status_code not in (200, 201):
            raise CalendarAPIError(
                f"POST 失败 {resp.status_code}: {resp.text[:200]}"
            )
        return resp.json()

    # ──────────────────────────── 公开 API ────────────────────────

    def list_calendars(self) -> list[dict]:
        """返回所有日历（原始 API 数据）。"""
        return self._get(_CAL_LIST_URL).get("items", [])

    def list_events(
        self,
        time_min: datetime,
        time_max: datetime,
    ) -> list[CalendarEvent]:
        """
        返回 [time_min, time_max) 范围内的结构化事件列表。
        两个参数必须是 timezone-aware datetime。
        """
        url = _EVENTS_URL.format(cal_id=self.calendar_id)
        params = {
            "singleEvents": "true",
            "orderBy":      "startTime",
            "timeMin":      _to_rfc3339(time_min),
            "timeMax":      _to_rfc3339(time_max),
        }
        items = self._get(url, params).get("items", [])
        return [_parse_event(item) for item in items]

    def get_event_status(self, event_id: str) -> str | None:
        """
        查询单个事件的 status 字段。
        返回 'confirmed' | 'cancelled' | None（事件不存在或查询失败）。

        用途：区分"日程自然结束消失"（不通知）与"被取消"（通知）。
        """
        url = _EVENTS_URL.format(cal_id=self.calendar_id) + f"/{event_id}"
        try:
            return self._get(url).get("status", "confirmed")
        except CalendarAPIError as e:
            if "404" in str(e):
                return None
            logger.warning(f"查询事件 {event_id} 状态失败: {e}")
            return None

    def create_event(
        self,
        summary: str,
        start: datetime,
        end: datetime | None = None,
        location: str = "",
        description: str = "",
        tz: str = "Asia/Shanghai",
    ) -> str:
        """
        创建日历事件。

        Args:
            summary:     事件标题
            start:       开始时间（timezone-aware）
            end:         结束时间；None 时默认持续 1 小时
            location:    地点（可选）
            description: 备注（可选）
            tz:          时区字符串

        Returns:
            创建成功后的 Google Calendar 事件 ID。

        Raises:
            CalendarAPIError: 创建失败时抛出。
        """
        if end is None:
            end = start + timedelta(hours=1)
        payload: dict[str, Any] = {
            "summary": summary,
            "start":   {"dateTime": _to_rfc3339(start), "timeZone": tz},
            "end":     {"dateTime": _to_rfc3339(end),   "timeZone": tz},
        }
        if location:
            payload["location"] = location
        if description:
            payload["description"] = description
        result = self._post(_EVENTS_URL.format(cal_id=self.calendar_id), payload)
        event_id = result.get("id", "")
        logger.info(f"事件已创建: {summary} (id={event_id})")
        return event_id

    def create_allday_event(
        self,
        summary: str,
        date: str,
        description: str = "",
        location: str = "",
    ) -> str:
        """
        创建全天事件（用于 ROC 周年、生日、体检等）。

        Args:
            summary:     事件标题
            date:        日期字符串 "YYYY-MM-DD"
            description: 备注（可选）
            location:    地点（可选）

        Returns:
            Google Calendar 事件 ID。
        """
        from datetime import date as _date
        d = datetime.strptime(date, "%Y-%m-%d")
        next_day = (d + timedelta(days=1)).strftime("%Y-%m-%d")
        payload: dict[str, Any] = {
            "summary": summary,
            "start":   {"date": date},
            "end":     {"date": next_day},
        }
        if description:
            payload["description"] = description
        if location:
            payload["location"] = location
        result = self._post(_EVENTS_URL.format(cal_id=self.calendar_id), payload)
        event_id = result.get("id", "")
        logger.info(f"全天事件已创建: {summary} ({date}) id={event_id}")
        return event_id

    def delete_event(self, event_id: str) -> None:
        """
        删除（取消）一个日历事件。

        Args:
            event_id: Google Calendar 事件 ID

        Raises:
            CalendarAPIError: 删除失败时抛出（404 = 事件不存在也视为成功）。
        """
        req = _require_requests()
        url = _EVENTS_URL.format(cal_id=self.calendar_id) + f"/{event_id}"
        resp = req.delete(
            url,
            headers=self._auth_headers(),
            proxies=self.proxies,
            verify=self.verify,
            timeout=self.timeout,
        )
        if resp.status_code in (200, 204, 404):
            logger.info(f"事件已删除: {event_id}")
            return
        raise CalendarAPIError(f"删除失败 {resp.status_code}: {resp.text[:200]}")


# ─────────────────────────── 内部工具函数 ─────────────────────────


def _to_rfc3339(dt: datetime) -> str:
    if dt.tzinfo is None:
        raise ValueError("datetime 必须携带时区信息，不能把 naive datetime 当作 UTC")
    return dt.isoformat().replace("+00:00", "Z")


def _parse_event(item: dict[str, Any]) -> CalendarEvent:
    """将 Google Calendar API 原始事件对象解析为 CalendarEvent。"""
    start_raw = item.get("start", {})
    end_raw   = item.get("end", {})
    is_all_day = "date" in start_raw and "dateTime" not in start_raw

    if is_all_day:
        start = _date_str_to_dt(start_raw["date"])
        end   = _date_str_to_dt(end_raw.get("date", start_raw["date"]))
    else:
        start = _parse_dt(start_raw.get("dateTime", ""))
        end   = _parse_dt(end_raw.get("dateTime", start_raw.get("dateTime", "")))

    return CalendarEvent(
        id          = item.get("id", ""),
        summary     = item.get("summary", "无标题"),
        start       = start,
        end         = end,
        status      = item.get("status", "confirmed"),
        is_all_day  = is_all_day,
        location    = item.get("location", ""),
        description = item.get("description", ""),
    )


def _parse_dt(s: str) -> datetime:
    if not s:
        return datetime.now(timezone.utc)
    dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt


def _date_str_to_dt(s: str) -> datetime:
    import sys
    from pathlib import Path
    sys.path.insert(0, str(Path(__file__).parent.parent))
    from compat import make_aware
    return make_aware(datetime.strptime(s, "%Y-%m-%d"), "Asia/Shanghai")


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