from __future__ import annotations

from dataclasses import dataclass
from datetime import date, datetime, time
from pathlib import Path
from typing import BinaryIO

from ..core.config import AppConfig, PayrollRules, load_config
from ..core.logger import AppLogger
from ..core.settings import AppSettings
from ..domain.calculator import PayrollCalculator
from ..domain.models import EmployeeAttendance, EmployeePayConfig, PayrollResult
from ..integrations.dingtalk import DingTalkAttendanceAdapter, DingTalkClient, DingTalkSettings
from ..io.exporter import export_payroll_results, localize_payroll_export_salary_modes
from ..io.parser import MonthlySummaryParser
from ..io.storage import FileStorage
from ..repositories import CommissionRepository, EmployeeRepository, PayrollRepository, SalaryProfileRepository
from .salary_config_service import SalaryConfigService

logger = AppLogger.get_logger(__name__)


class PayrollService:
    """纯工资服务：只负责把考勤输入转换成工资计算结果。"""

    def __init__(self, config: AppConfig):
        self.config = config
        self.calculator = PayrollCalculator(config)

    def calculate_from_excel(self, excel_path: str | Path) -> list[PayrollResult]:
        logger.info("Excel 工资计算开始 excel_path=%s", excel_path)
        parser = MonthlySummaryParser(self.config.attendance)
        employees = parser.parse(excel_path)
        results = self.calculator.calculate(employees)
        logger.info("Excel 工资计算完成 excel_path=%s employee_count=%s", excel_path, len(results))
        return results


@dataclass(frozen=True)
class PayrollComputation:
    """一次工资计算的返回值，供 API schema 和持久化流程复用。"""

    job_id: str
    results: list[PayrollResult]
    output_file: str | None = None

    @property
    def employee_count(self) -> int:
        return len(self.results)

    @property
    def output_filename(self) -> str | None:
        return Path(self.output_file).name if self.output_file else None


class PayrollApplicationService:
    """应用服务层：编排上传、外部接口、计算、导出和落库。"""

    def __init__(
        self,
        repository: PayrollRepository,
        settings: AppSettings,
        salary_repository: SalaryProfileRepository | None = None,
        commission_repository: CommissionRepository | None = None,
        config_service: SalaryConfigService | None = None,
        employee_repository: EmployeeRepository | None = None,
    ):
        self.repository = repository
        self.settings = settings
        self.salary_repository = salary_repository
        self.commission_repository = commission_repository
        self.config_service = config_service
        self.employee_repository = employee_repository
        self.storage = FileStorage(settings)

    def calculate_from_excel_upload(
        self,
        filename: str,
        stream: BinaryIO,
        config_path: str | None = None,
        export_excel: bool = True,
    ) -> PayrollComputation:
        logger.info("Excel 上传计算请求开始 filename=%s export_excel=%s", filename, export_excel)
        upload_path = self.storage.save_upload(filename, stream)
        job = self.repository.create_job("excel", input_file=str(upload_path))
        logger.info("Excel 工资计算任务已创建 job_id=%s upload_path=%s", job.id, upload_path)
        try:
            config = self._load_runtime_config(config_path)
            employees = MonthlySummaryParser(config.attendance).parse(upload_path)
            self._enrich_employees_from_maintenance(employees)
            salary_month = self._salary_month_from_employees(employees)
            config = self._load_runtime_config(config_path, salary_month=salary_month)
            results = PayrollCalculator(config).calculate(employees)
            output_file = self._export_if_needed(config, results, "excel", export_excel)
            self.repository.save_results(job.id, results)
            self.repository.mark_completed(job.id, len(results), output_file)
            logger.info(
                "Excel 工资计算任务完成 job_id=%s employee_count=%s output_file=%s",
                job.id,
                len(results),
                output_file,
            )
            return PayrollComputation(job_id=job.id, results=results, output_file=output_file)
        except Exception as exc:
            self.repository.mark_failed(job.id, str(exc))
            logger.exception("Excel 工资计算任务失败 job_id=%s upload_path=%s", job.id, upload_path)
            raise

    async def calculate_from_dingtalk_realtime(
        self,
        user_ids: list[str],
        start_date: date,
        end_date: date,
        config_path: str | None = None,
        export_excel: bool = True,
    ) -> PayrollComputation:
        logger.info(
            "钉钉实时工资计算请求开始 user_count=%s start_date=%s end_date=%s export_excel=%s",
            len(user_ids),
            start_date,
            end_date,
            export_excel,
        )
        config = self._load_runtime_config(config_path, salary_month=start_date.strftime("%Y-%m"))
        settings = self._dingtalk_settings()
        job = self.repository.create_job("dingtalk")
        logger.info("钉钉工资计算任务已创建 job_id=%s", job.id)
        try:
            client = DingTalkClient(settings)
            start_dt = datetime.combine(start_date, time.min)
            end_dt = datetime.combine(end_date, time(23, 59, 59))
            records = await client.list_attendance_records(user_ids, start_dt, end_dt)
            adapter = DingTalkAttendanceAdapter(config.attendance, timezone=settings.timezone)
            employees = adapter.to_employee_attendance(records)
            self._enrich_employees_from_maintenance(employees)
            results = PayrollCalculator(config).calculate(employees)
            output_file = self._export_if_needed(config, results, "dingtalk", export_excel)
            self.repository.save_results(job.id, results)
            self.repository.mark_completed(job.id, len(results), output_file)
            logger.info(
                "钉钉工资计算任务完成 job_id=%s record_count=%s employee_count=%s output_file=%s",
                job.id,
                len(records),
                len(results),
                output_file,
            )
            return PayrollComputation(job_id=job.id, results=results, output_file=output_file)
        except Exception as exc:
            self.repository.mark_failed(job.id, str(exc))
            logger.exception("钉钉工资计算任务失败 job_id=%s", job.id)
            raise

    def get_job(self, job_id: str):
        job = self.repository.get_job(job_id)
        logger.info("查询工资计算任务 job_id=%s found=%s", job_id, job is not None)
        return job

    def resolve_output_file(self, filename: str) -> Path:
        logger.info("解析工资结果下载文件 filename=%s", filename)
        return self.storage.resolve_output_file(filename)

    def localize_export_file(self, source_path: str | Path, source_type: str) -> Path:
        """生成一份用户下载用工资表副本，修正历史文件中的薪资模式编码。"""
        output_path = self.storage.payroll_output_path(source_type)
        localize_payroll_export_salary_modes(source_path, output_path)
        logger.info("工资表导出副本已中文化 source_path=%s output_path=%s", source_path, output_path)
        return output_path

    def load_runtime_config(self, config_path: str | None, salary_month: str | None = None) -> AppConfig:
        """对外提供当前数据库规则合并后的工资配置。"""
        return self._load_runtime_config(config_path, salary_month=salary_month)

    def dingtalk_settings(self) -> DingTalkSettings:
        """对外提供钉钉配置读取，统一错误提示。"""
        return self._dingtalk_settings()

    def _export_if_needed(
        self,
        config: AppConfig,
        results: list[PayrollResult],
        source_type: str,
        export_excel: bool,
    ) -> str | None:
        if not export_excel:
            logger.info("跳过工资结果导出 source_type=%s", source_type)
            return None
        output_path = self.storage.payroll_output_path(source_type)
        export_payroll_results(results, config, output_path)
        logger.info("工资结果导出完成 source_type=%s output_path=%s", source_type, output_path)
        return str(output_path)

    def _load_runtime_config(self, config_path: str | None, salary_month: str | None = None) -> AppConfig:
        config = load_config(config_path)
        if self.config_service is not None:
            self.config_service.ensure_defaults()
            config = self.config_service.resolve_app_config(config)
        if self.salary_repository is None:
            return config

        pay_configs = dict(config.payroll.employees)
        profiles = self.salary_repository.list_active_profiles()
        month_commissions = (
            self.commission_repository.aggregate_by_month(salary_month)
            if self.commission_repository is not None and salary_month
            else {}
        )
        for profile in profiles:
            employee = profile.employee
            if not employee.name:
                continue
            pay_configs[employee.name] = EmployeePayConfig(
                salary_mode=profile.salary_mode,
                base_salary=profile.base_salary,
                hourly_rate=profile.hourly_rate,
                piece_quantity=profile.piece_quantity,
                piece_unit_price=profile.piece_unit_price,
                probation_type=profile.probation_type,
                probation_ratio=profile.probation_ratio,
                probation_salary=profile.probation_salary or None,
                commission_amount=profile.commission_amount + month_commissions.get(employee.id, 0),
                overtime_rate=profile.overtime_rate,
                weekend_overtime_rate=profile.weekend_overtime_rate,
                holiday_overtime_rate=profile.holiday_overtime_rate,
            )
        if profiles:
            logger.info(
                "已合并数据库薪资档案到工资计算配置 count=%s salary_month=%s commission_employee_count=%s",
                len(profiles),
                salary_month,
                len(month_commissions),
            )
        return AppConfig(
            attendance=config.attendance,
            payroll=PayrollRules(
                default_overtime_rate=config.payroll.default_overtime_rate,
                default_weekend_overtime_rate=config.payroll.default_weekend_overtime_rate,
                default_holiday_overtime_rate=config.payroll.default_holiday_overtime_rate,
                employees=pay_configs,
            ),
        )

    def _enrich_employees_from_maintenance(self, employees: list[EmployeeAttendance]) -> None:
        """优先使用员工维护中的员工编号、部门和岗位，未维护时保留 Excel/钉钉原值。"""
        if self.employee_repository is None or not employees:
            return

        maintained_employees = self.employee_repository.list_employees()
        by_employee_no = {
            _clean_key(employee.employee_no): employee
            for employee in maintained_employees
            if _clean_key(employee.employee_no)
        }
        by_dingtalk_user_id = {
            _clean_key(employee.dingtalk_user_id): employee
            for employee in maintained_employees
            if _clean_key(employee.dingtalk_user_id)
        }
        by_name = {
            _clean_key(employee.name): employee
            for employee in maintained_employees
            if _clean_key(employee.name)
        }

        enriched_count = 0
        for employee in employees:
            maintained = (
                by_employee_no.get(_clean_key(employee.employee_no))
                or by_dingtalk_user_id.get(_clean_key(employee.user_id))
                or by_name.get(_clean_key(employee.name))
            )
            if maintained is None:
                continue

            employee.employee_no = maintained.employee_no or employee.employee_no
            employee.name = maintained.name or employee.name
            employee.user_id = maintained.dingtalk_user_id or employee.user_id
            employee.department = maintained.department or employee.department
            employee.position = maintained.position or employee.position
            enriched_count += 1

        if enriched_count:
            logger.info("已使用员工维护数据补齐工资计算员工信息 count=%s", enriched_count)

    def _salary_month_from_employees(self, employees: list[EmployeeAttendance]) -> str | None:
        for employee in employees:
            if employee.daily_records:
                return employee.daily_records[0].work_date.strftime("%Y-%m")
        return None

    def _dingtalk_settings(self) -> DingTalkSettings:
        if not self.settings.dingtalk_app_key or not self.settings.dingtalk_app_secret:
            logger.error("钉钉配置缺失 config_path=%s", self.settings.config_path)
            raise ValueError("请先在 config/app_settings.json 中配置 dingtalk.app_key 和 dingtalk.app_secret")
        return DingTalkSettings(
            app_key=self.settings.dingtalk_app_key,
            app_secret=self.settings.dingtalk_app_secret,
            base_url=self.settings.dingtalk_base_url,
            timezone=self.settings.dingtalk_timezone,
        )


def _clean_key(value: object) -> str:
    return str(value or "").strip()
