from __future__ import annotations

from dataclasses import dataclass
from datetime import date
from decimal import Decimal, ROUND_HALF_UP
from typing import Protocol, runtime_checkable

from django.core.exceptions import ValidationError
from django.db import models

from flower.common import ModelBase


class CostCategory(ModelBase):
    """支出类目"""

    merchant = models.ForeignKey(
        'basic_info.Merchant',
        on_delete=models.PROTECT,
        related_name='cost_categories',
        verbose_name='所属商户',
    )
    unique_key = models.CharField(max_length=100, verbose_name='唯一标识键')
    name = models.CharField(max_length=100, verbose_name='类目名称')
    parent = models.ForeignKey(
        'self',
        on_delete=models.PROTECT,
        null=True,
        blank=True,
        related_name='children',
        verbose_name='父类目',
    )
    description = models.TextField(null=True, blank=True, verbose_name='描述')

    class Meta:
        verbose_name = '支出类目'
        verbose_name_plural = '支出类目'
        unique_together = ('merchant', 'unique_key')
        ordering = ('merchant', 'name')

    def __str__(self):
        return f'{self.name} ({self.unique_key})'


class CostEntry(ModelBase):
    """支出明细"""

    AMOUNT_QUANT = Decimal('0.01')

    merchant = models.ForeignKey(
        'basic_info.Merchant',
        on_delete=models.PROTECT,
        related_name='cost_entries',
        verbose_name='所属商户',
    )
    category = models.ForeignKey(
        CostCategory,
        on_delete=models.PROTECT,
        related_name='entries',
        verbose_name='支出类目',
    )
    amount = models.DecimalField(max_digits=15, decimal_places=2, blank=True, verbose_name='金额')
    unit_amount = models.DecimalField(
        max_digits=15,
        decimal_places=4,
        null=True,
        blank=True,
        verbose_name='单价/基数金额',
    )
    quantity = models.DecimalField(
        max_digits=12,
        decimal_places=4,
        null=True,
        blank=True,
        verbose_name='数量/倍数',
    )
    unit_name = models.CharField(
        max_length=20,
        null=True,
        blank=True,
        verbose_name='单位名称',
    )
    occurred_at = models.DateField(verbose_name='发生日期')
    operator = models.ForeignKey(
        'basic_info.Employee',
        on_delete=models.PROTECT,
        null=True,
        blank=True,
        related_name='cost_entries',
        verbose_name='经办人',
    )
    image1 = models.ImageField(null=True, blank=True, upload_to='cost/entries/', verbose_name='凭证图片')
    image2 = models.ImageField(null=True, blank=True, upload_to='cost/entries/', verbose_name='备用凭证图片')
    source_module = models.CharField(
        max_length=50, null=True, blank=True, verbose_name='来源模块',
    )
    source_id = models.CharField(
        max_length=100, null=True, blank=True, verbose_name='来源记录ID',
    )
    remarks = models.TextField(null=True, blank=True, verbose_name='备注')

    class Meta:
        verbose_name = '支出明细'
        verbose_name_plural = '支出明细'
        ordering = ('-occurred_at', '-created_at')

    def __str__(self):
        return f'{self.category.name} {self.amount} ({self.occurred_at})'

    @property
    def has_amount_formula(self) -> bool:
        """是否使用 unit_amount * quantity 推导最终金额。"""
        return self.unit_amount is not None or self.quantity is not None

    def calculate_amount(self) -> Decimal | None:
        """根据单价和数量计算最终金额。"""
        if self.unit_amount is None and self.quantity is None:
            return None
        if self.unit_amount is None or self.quantity is None:
            raise ValidationError({
                'unit_amount': 'unit_amount 和 quantity 必须同时填写或同时为空',
                'quantity': 'unit_amount 和 quantity 必须同时填写或同时为空',
            })
        return (self.unit_amount * self.quantity).quantize(self.AMOUNT_QUANT, rounding=ROUND_HALF_UP)

    def apply_amount_formula(self) -> None:
        """如果存在公式字段，则用公式结果覆盖 amount。"""
        calculated_amount = self.calculate_amount()
        if calculated_amount is not None:
            self.amount = calculated_amount

    def clean(self):
        super().clean()
        self.apply_amount_formula()
        if self.amount is None:
            raise ValidationError({'amount': '普通支出必须填写 amount'})

    def save(self, *args, **kwargs):
        self.clean()
        update_fields = kwargs.get('update_fields')
        if update_fields is not None and self.has_amount_formula:
            kwargs['update_fields'] = set(update_fields) | {'amount'}
        return super().save(*args, **kwargs)


# ==================== 六边形端口协议 ====================


@dataclass(slots=True)
class CostEntryInput:
    """Provider 返回的结构化成本条目"""

    category_key: str
    category_name: str
    amount: Decimal | None
    occurred_at: date
    source_module: str
    source_id: str
    unit_amount: Decimal | None = None
    quantity: Decimal | None = None
    unit_name: str = ''
    remarks: str = ''


@runtime_checkable
class CostProviderPort(Protocol):
    """
    成本数据提供者接口。

    其它模块实现此接口即可被 cost.services.collect_from_provider() 统一采集。

    Provider 可以通过 category_key 声明自己使用的类目：
      - 如果 cost 模块已有匹配的 category_key，直接使用
      - 如果没有，cost 模块会调用 ensure_category() 自动创建

    Provider 也可以反向依赖 cost 模块的基础能力：
      - from cost.services import ensure_category
      - 在实现 get_cost_entries 前主动调用 ensure_category 预建类目

    使用示例::

        from cost.models import CostProviderPort, CostEntryInput
        from cost.services import ensure_category

        class PrintingCostProvider:

            category_key = 'printing_consumables'

            def get_cost_entries(self, *, merchant, start_date, end_date):
                ensure_category(
                    merchant=merchant,
                    category_key=self.category_key,
                    category_name='印刷耗材',
                )
                return [
                    CostEntryInput(
                        category_key=self.category_key,
                        category_name='印刷耗材',
                        amount=Decimal('100.00'),
                        occurred_at=date.today(),
                        source_module='printing',
                        source_id='PJ-001',
                    ),
                ]
    """

    category_key: str

    def get_cost_entries(
        self, *, merchant, start_date: date, end_date: date
    ) -> list[CostEntryInput]:
        ...