from __future__ import annotations

from dataclasses import asdict, dataclass
from datetime import date, datetime, time, timedelta

from ..core.logger import AppLogger
from ..database.orm import AttendanceRecordORM, EmployeeORM, SalaryPreviewORM
from ..domain.calculator import PayrollCalculator
from ..domain.models import DailyAttendance, EmployeeAttendance, PayrollResult
from ..integrations.dingtalk import DingTalkAttendanceAdapter, DingTalkClient
from ..repositories import RealtimeAttendanceRepository
from .payroll_service import PayrollApplicationService

logger = AppLogger.get_logger(__name__)


@dataclass(frozen=True)
class TodayAttendanceItemData:
    employee_id: int | None
    employee_no: str
    employee_name: str
    department: str
    position: str
    first_punch: str
    last_punch: str
    attendance_status: str
    late_minutes: int
    missing_card_count: int
    leave_hours: float
    today_overtime_hours: float
    month_remaining_overtime_hours: float
    estimated_net_salary: float | None
    note: str


@dataclass(frozen=True)
class RealtimeAttendanceSummaryData:
    attendance_date: date
    employee_total: int
    checked_in_count: int
    unchecked_count: int
    late_count: int
    leave_count: int
    missing_card_count: int
    estimated_overtime_count: int
    estimated_net_total: float


@dataclass(frozen=True)
class RealtimeAttendanceResponseData:
    sync_job_id: str | None
    synced_at: datetime | None
    summary: RealtimeAttendanceSummaryData
    items: list[TodayAttendanceItemData]

    def to_dict(self) -> dict:
        return asdict(self)


class RealtimeAttendanceService:
    """实时考勤应用服务：同步钉钉、落库标准考勤，并生成薪资预估看板。"""

    def __init__(
        self,
        repository: RealtimeAttendanceRepository,
        payroll_service: PayrollApplicationService,
    ):
        self.repository = repository
        self.payroll_service = payroll_service

    def get_today(self, attendance_date: date) -> RealtimeAttendanceResponseData:
        logger.info("实时考勤看板查询开始 attendance_date=%s", attendance_date)
        employees = self.repository.list_active_employees()
        records = self.repository.list_today_records(attendance_date)
        previews = self.repository.list_salary_previews(attendance_date.strftime("%Y-%m"))
        latest_job = self.repository.latest_completed_job(attendance_date)
        items = self._build_today_items(attendance_date, employees, records, previews)
        logger.info("实时考勤看板查询完成 attendance_date=%s employee_count=%s", attendance_date, len(items))
        return RealtimeAttendanceResponseData(
            sync_job_id=latest_job.id if latest_job else None,
            synced_at=latest_job.updated_at if latest_job else None,
            summary=build_realtime_summary(attendance_date, items),
            items=items,
        )

    async def sync_today(
        self,
        *,
        attendance_date: date,
        created_by: str,
        include_salary_preview: bool = True,
    ) -> RealtimeAttendanceResponseData:
        employees = self.repository.list_active_employees_with_dingtalk()
        if not employees:
            logger.error("实时考勤同步失败 reason=no_dingtalk_user_id")
            raise ValueError("没有绑定钉钉 userId 的在职员工，请先在员工维护中补齐钉钉用户ID")

        salary_month = attendance_date.strftime("%Y-%m")
        start_date = attendance_date.replace(day=1)
        job = self.repository.create_sync_job(
            source_type="dingtalk",
            sync_mode="month_to_date",
            attendance_date=attendance_date,
            salary_month=salary_month,
            start_date=start_date,
            end_date=attendance_date,
            created_by=created_by,
        )
        logger.info(
            "实时考勤同步任务创建 job_id=%s attendance_date=%s employee_count=%s",
            job.id,
            attendance_date,
            len(employees),
        )

        try:
            settings = self.payroll_service.dingtalk_settings()
            config = self.payroll_service.load_runtime_config(None, salary_month=salary_month)
            client = DingTalkClient(settings)
            raw_records = await client.list_attendance_records(
                [employee.dingtalk_user_id for employee in employees],
                datetime.combine(start_date, time.min),
                datetime.combine(attendance_date, time(23, 59, 59)),
            )
            adapter = DingTalkAttendanceAdapter(config.attendance, timezone=settings.timezone)
            adapted_employees = adapter.to_employee_attendance(raw_records)
            attendance = self._merge_employee_directory(
                employees=employees,
                adapted_employees=adapted_employees,
                start_date=start_date,
                end_date=attendance_date,
            )
            results = PayrollCalculator(config).calculate(attendance)
            employee_by_no = {employee.employee_no: employee for employee in employees}
            employee_ids = [employee.id for employee in employees]
            self.repository.replace_attendance_records(
                source_type="dingtalk",
                start_date=start_date,
                end_date=attendance_date,
                employee_ids=employee_ids,
                rows=self._attendance_rows(results, employee_by_no),
            )
            if include_salary_preview:
                self.repository.replace_salary_previews(
                    job.id,
                    self._salary_preview_rows(job.id, results, employee_by_no),
                )
            self.repository.mark_sync_completed(
                job.id,
                employee_count=len(employees),
                record_count=len(raw_records),
            )
            logger.info(
                "实时考勤同步任务完成 job_id=%s raw_record_count=%s employee_count=%s",
                job.id,
                len(raw_records),
                len(employees),
            )
        except Exception as exc:
            self.repository.mark_sync_failed(job.id, str(exc))
            logger.exception("实时考勤同步任务失败 job_id=%s", job.id)
            raise

        return self.get_today(attendance_date)

    def _merge_employee_directory(
        self,
        *,
        employees: list[EmployeeORM],
        adapted_employees: list[EmployeeAttendance],
        start_date: date,
        end_date: date,
    ) -> list[EmployeeAttendance]:
        adapted_by_user_id = {employee.user_id: employee for employee in adapted_employees}
        days = list(_date_range(start_date, end_date))
        merged: list[EmployeeAttendance] = []
        for employee in employees:
            adapted = adapted_by_user_id.get(employee.dingtalk_user_id)
            daily_by_date = {
                record.work_date: record
                for record in adapted.daily_records
            } if adapted else {}
            merged.append(
                EmployeeAttendance(
                    name=employee.name,
                    attendance_group="钉钉实时考勤",
                    department=employee.department,
                    employee_no=employee.employee_no,
                    position=employee.position,
                    user_id=employee.dingtalk_user_id,
                    daily_records=[daily_by_date.get(day) or DailyAttendance(work_date=day) for day in days],
                )
            )
        return merged

    def _build_today_items(
        self,
        attendance_date: date,
        employees: list[EmployeeORM],
        records: list[AttendanceRecordORM],
        previews: list[SalaryPreviewORM],
    ) -> list[TodayAttendanceItemData]:
        records_by_employee_id = {
            record.employee_id: record
            for record in records
            if record.employee_id is not None
        }
        records_by_employee_no = {record.employee_no: record for record in records if record.employee_no}
        previews_by_employee_id: dict[int, SalaryPreviewORM] = {}
        previews_by_employee_no: dict[str, SalaryPreviewORM] = {}
        for preview in previews:
            if preview.employee_id is not None and preview.employee_id not in previews_by_employee_id:
                previews_by_employee_id[preview.employee_id] = preview
            if preview.employee_no and preview.employee_no not in previews_by_employee_no:
                previews_by_employee_no[preview.employee_no] = preview

        items = [
            self._today_item_from_employee(
                attendance_date,
                employee,
                records_by_employee_id.get(employee.id) or records_by_employee_no.get(employee.employee_no),
                previews_by_employee_id.get(employee.id) or previews_by_employee_no.get(employee.employee_no),
            )
            for employee in employees
        ]
        known_employee_ids = {employee.id for employee in employees}
        for record in records:
            if record.employee_id in known_employee_ids:
                continue
            items.append(self._today_item_from_record(record, previews_by_employee_no.get(record.employee_no)))
        return items

    def _today_item_from_employee(
        self,
        attendance_date: date,
        employee: EmployeeORM,
        record: AttendanceRecordORM | None,
        preview: SalaryPreviewORM | None,
    ) -> TodayAttendanceItemData:
        if record is None:
            note = "未绑定钉钉 userId" if not employee.dingtalk_user_id else ""
            return TodayAttendanceItemData(
                employee_id=employee.id,
                employee_no=employee.employee_no,
                employee_name=employee.name,
                department=employee.department,
                position=employee.position,
                first_punch="",
                last_punch="",
                attendance_status="unchecked",
                late_minutes=0,
                missing_card_count=0,
                leave_hours=0,
                today_overtime_hours=0,
                month_remaining_overtime_hours=preview.remaining_overtime_hours if preview else 0,
                estimated_net_salary=preview.estimated_net_salary if preview else None,
                note=note,
            )
        return self._today_item_from_record(record, preview, fallback_position=employee.position)

    def _today_item_from_record(
        self,
        record: AttendanceRecordORM,
        preview: SalaryPreviewORM | None,
        fallback_position: str = "",
    ) -> TodayAttendanceItemData:
        return TodayAttendanceItemData(
            employee_id=record.employee_id,
            employee_no=record.employee_no,
            employee_name=record.employee_name,
            department=record.department,
            position=preview.position if preview and preview.position else fallback_position,
            first_punch=record.first_punch,
            last_punch=record.last_punch,
            attendance_status=_attendance_status(record),
            late_minutes=record.late_minutes,
            missing_card_count=record.missing_card_count,
            leave_hours=record.leave_hours,
            today_overtime_hours=record.overtime_hours,
            month_remaining_overtime_hours=preview.remaining_overtime_hours if preview else 0,
            estimated_net_salary=preview.estimated_net_salary if preview else None,
            note=preview.note if preview else "",
        )

    def _attendance_rows(
        self,
        results: list[PayrollResult],
        employee_by_no: dict[str, EmployeeORM],
    ) -> list[AttendanceRecordORM]:
        rows: list[AttendanceRecordORM] = []
        for result in results:
            employee = employee_by_no.get(result.employee.employee_no)
            for record in result.employee.daily_records:
                rows.append(
                    AttendanceRecordORM(
                        employee_id=employee.id if employee else None,
                        employee_no=result.employee.employee_no,
                        employee_name=result.employee.name,
                        department=result.employee.department,
                        work_date=record.work_date,
                        first_punch=_time_text(record.first_punch),
                        last_punch=_time_text(record.last_punch),
                        attendance_status=_daily_status(record),
                        late_minutes=sum(record.late_minutes),
                        missing_card_count=record.missing_card_count,
                        leave_hours=record.leave_hours_in_cell,
                        overtime_hours=record.punch_overtime_hours,
                        raw_text=record.raw_text,
                        source_type="dingtalk",
                    )
                )
        return rows

    def _salary_preview_rows(
        self,
        sync_job_id: str,
        results: list[PayrollResult],
        employee_by_no: dict[str, EmployeeORM],
    ) -> list[SalaryPreviewORM]:
        rows: list[SalaryPreviewORM] = []
        for result in results:
            employee = employee_by_no.get(result.employee.employee_no)
            rows.append(
                SalaryPreviewORM(
                    sync_job_id=sync_job_id,
                    employee_id=employee.id if employee else None,
                    employee_no=result.employee.employee_no,
                    employee_name=result.employee.name,
                    department=result.employee.department,
                    position=result.employee.position,
                    salary_month=result.salary_month,
                    attendance_days=result.attendance_days,
                    actual_work_hours=result.actual_work_hours,
                    punch_overtime_hours=result.punch_overtime_hours,
                    leave_hours=result.leave_hours,
                    remaining_overtime_hours=result.remaining_overtime_hours,
                    late_deduction=result.late_deduction,
                    missing_card_deduction=result.missing_card_deduction,
                    commission_amount=result.commission_amount,
                    overtime_pay=result.overtime_pay,
                    estimated_gross_salary=result.gross_salary,
                    estimated_net_salary=result.net_salary,
                    status="warning" if result.note or result.remaining_overtime_hours < 0 else "estimated",
                    note=result.note,
                )
            )
        return rows


def build_realtime_summary(
    attendance_date: date,
    items: list[TodayAttendanceItemData],
) -> RealtimeAttendanceSummaryData:
    """按页面行聚合实时考勤摘要，保持统计口径集中可测。"""
    checked_in_count = sum(1 for item in items if item.first_punch or item.last_punch)
    estimated_net_total = round(
        sum(item.estimated_net_salary or 0 for item in items),
        2,
    )
    return RealtimeAttendanceSummaryData(
        attendance_date=attendance_date,
        employee_total=len(items),
        checked_in_count=checked_in_count,
        unchecked_count=max(0, len(items) - checked_in_count),
        late_count=sum(1 for item in items if item.late_minutes > 0),
        leave_count=sum(1 for item in items if item.leave_hours > 0),
        missing_card_count=sum(item.missing_card_count for item in items),
        estimated_overtime_count=sum(1 for item in items if item.today_overtime_hours > 0),
        estimated_net_total=estimated_net_total,
    )


def _date_range(start_date: date, end_date: date):
    current = start_date
    while current <= end_date:
        yield current
        current += timedelta(days=1)


def _time_text(value) -> str:
    return value.strftime("%H:%M") if value else ""


def _daily_status(record: DailyAttendance) -> str:
    if record.first_punch or record.last_punch:
        return "missing" if record.missing_card_count else "present"
    if record.leave_hours_in_cell:
        return "leave"
    return "unchecked"


def _attendance_status(record: AttendanceRecordORM) -> str:
    if record.attendance_status == "absent":
        return "unchecked"
    return record.attendance_status or "unchecked"
