from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass
from datetime import date, datetime, time
from typing import Any, Iterable
from zoneinfo import ZoneInfo

import httpx

from ..core.config import AttendanceRules
from ..core.logger import AppLogger
from ..domain.models import DailyAttendance, EmployeeAttendance

logger = AppLogger.get_logger(__name__)


class DingTalkError(RuntimeError):
    pass


@dataclass(frozen=True)
class DingTalkSettings:
    app_key: str
    app_secret: str
    base_url: str = "https://oapi.dingtalk.com"
    timeout_seconds: float = 15.0
    timezone: str = "Asia/Shanghai"


class DingTalkClient:
    """钉钉 OpenAPI 客户端，只负责 HTTP 请求和接口错误转换。"""

    def __init__(self, settings: DingTalkSettings):
        self.settings = settings

    async def get_access_token(self) -> str:
        logger.info("钉钉 access_token 请求开始 base_url=%s", self.settings.base_url)
        async with httpx.AsyncClient(
            base_url=self.settings.base_url,
            timeout=self.settings.timeout_seconds,
        ) as client:
            response = await client.get(
                "/gettoken",
                params={"appkey": self.settings.app_key, "appsecret": self.settings.app_secret},
            )
        payload = response.json()
        self._raise_for_dingtalk_error(payload)
        token = payload.get("access_token")
        if not token:
            logger.error("钉钉 access_token 响应缺失 payload=%s", payload)
            raise DingTalkError(f"钉钉未返回 access_token：{payload}")
        logger.info("钉钉 access_token 请求成功")
        return token

    async def list_attendance_records(
        self,
        user_ids: list[str],
        start_time: datetime,
        end_time: datetime,
    ) -> list[dict[str, Any]]:
        logger.info(
            "钉钉考勤记录请求开始 user_count=%s start_time=%s end_time=%s",
            len(user_ids),
            start_time,
            end_time,
        )
        token = await self.get_access_token()
        body = {
            "userIds": user_ids,
            "checkDateFrom": start_time.strftime("%Y-%m-%d %H:%M:%S"),
            "checkDateTo": end_time.strftime("%Y-%m-%d %H:%M:%S"),
        }
        async with httpx.AsyncClient(
            base_url=self.settings.base_url,
            timeout=self.settings.timeout_seconds,
        ) as client:
            response = await client.post(
                "/topapi/attendance/listRecord",
                params={"access_token": token},
                json=body,
            )
        payload = response.json()
        self._raise_for_dingtalk_error(payload)
        records = payload.get("recordresult") or payload.get("result", {}).get("recordresult") or []
        logger.info("钉钉考勤记录请求成功 record_count=%s", len(records))
        return records

    def _raise_for_dingtalk_error(self, payload: dict[str, Any]) -> None:
        errcode = payload.get("errcode")
        if errcode not in (None, 0):
            errmsg = payload.get("errmsg", "未知错误")
            logger.error("钉钉接口返回错误 errcode=%s errmsg=%s", errcode, errmsg)
            raise DingTalkError(f"钉钉接口错误 {errcode}: {errmsg}")


class DingTalkAttendanceAdapter:
    """把钉钉实时打卡记录适配成工资计算器可消费的考勤模型。"""

    def __init__(self, rules: AttendanceRules, timezone: str = "Asia/Shanghai"):
        self.timezone = ZoneInfo(timezone)
        self.on_work_time = datetime.strptime(rules.on_work_time, "%H:%M").time()

    def to_employee_attendance(self, records: Iterable[dict[str, Any]]) -> list[EmployeeAttendance]:
        records = list(records)
        logger.info("钉钉考勤记录适配开始 record_count=%s", len(records))
        grouped: dict[str, dict[date, list[dict[str, Any]]]] = defaultdict(lambda: defaultdict(list))
        names: dict[str, str] = {}
        departments: dict[str, str] = {}

        for record in records:
            user_id = str(record.get("userId") or record.get("userid") or "").strip()
            if not user_id:
                continue
            check_dt = self._record_datetime(record)
            if check_dt is None:
                continue
            grouped[user_id][check_dt.date()].append(record)
            names[user_id] = str(record.get("userName") or record.get("name") or user_id)
            departments[user_id] = str(record.get("departmentName") or record.get("department") or "")

        employees: list[EmployeeAttendance] = []
        for user_id, by_day in grouped.items():
            employee = EmployeeAttendance(
                name=names.get(user_id, user_id),
                user_id=user_id,
                department=departments.get(user_id, ""),
                attendance_group="钉钉实时考勤",
            )
            for work_date in sorted(by_day):
                employee.daily_records.append(self._build_daily_record(work_date, by_day[work_date]))
            employees.append(employee)
        logger.info("钉钉考勤记录适配完成 employee_count=%s", len(employees))
        return employees

    def _build_daily_record(self, work_date: date, records: list[dict[str, Any]]) -> DailyAttendance:
        dated_records = [
            (check_dt, record)
            for record in records
            if (check_dt := self._record_datetime(record)) is not None
        ]
        dated_records.sort(key=lambda item: item[0])
        punch_times = [check_dt.time() for check_dt, _ in dated_records]
        raw_text = "；".join(
            self._record_summary(record, check_dt) for check_dt, record in dated_records
        )
        first_punch = punch_times[0] if punch_times else None
        late_minutes = self._late_minutes(first_punch)
        return DailyAttendance(
            work_date=work_date,
            raw_text=raw_text,
            first_punch=first_punch,
            last_punch=punch_times[-1] if punch_times else None,
            late_minutes=[late_minutes] if late_minutes > 0 else [],
            missing_card_count=sum(1 for item in records if self._is_missing_card(item)),
        )

    def _record_datetime(self, record: dict[str, Any]) -> datetime | None:
        value = record.get("userCheckTime") or record.get("checkTime") or record.get("baseCheckTime")
        if value in (None, ""):
            return None
        if isinstance(value, (int, float)):
            return datetime.fromtimestamp(value / 1000, tz=self.timezone).replace(tzinfo=None)
        text = str(value).strip()
        for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M"):
            try:
                return datetime.strptime(text[:19], fmt)
            except ValueError:
                continue
        return None

    def _record_summary(self, record: dict[str, Any], check_dt: datetime | None) -> str:
        check_time = check_dt.strftime("%H:%M") if check_dt else "-"
        check_type = record.get("checkType") or record.get("type") or ""
        time_result = record.get("timeResult") or ""
        location_result = record.get("locationResult") or ""
        return f"{check_type} {check_time} {time_result} {location_result}".strip()

    def _late_minutes(self, first_punch: time | None) -> int:
        if first_punch is None:
            return 0
        punch_minutes = first_punch.hour * 60 + first_punch.minute
        on_work_minutes = self.on_work_time.hour * 60 + self.on_work_time.minute
        return max(0, punch_minutes - on_work_minutes)

    def _is_missing_card(self, record: dict[str, Any]) -> bool:
        values = {
            str(record.get("timeResult") or ""),
            str(record.get("checkType") or ""),
            str(record.get("sourceType") or ""),
        }
        joined = " ".join(values).lower()
        return "notsigned" in joined or "缺卡" in joined
