from datetime import date, datetime
from decimal import Decimal
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, computed_field


class InvoiceClientReviewInput(BaseModel):
    """Resultado de la revisión de la factura por el cliente (R-F-06)."""
    approved: bool
    notes: str | None = None


class InvoiceItemSatFields(BaseModel):
    """Claves fiscales de la partida. Opcionales: las facturas previas no las tienen."""

    concept_id: int | None = None
    product_service_id: int | None = None
    unit_of_measure_id: int | None = None
    tax_object_id: int | None = None


class InvoiceItemBase(InvoiceItemSatFields):
    concept: str = Field(..., max_length=60)
    description: str | None = Field(None, max_length=255)
    quantity: Decimal = Field(Decimal(1), ge=0, max_digits=12, decimal_places=2)
    unit_amount: Decimal = Field(Decimal(0), ge=0, max_digits=14, decimal_places=2)


class InvoiceItemCreate(InvoiceItemBase):
    invoice_id: int
    # Opcional solo si viene concept_id: el service copia la descripción del concepto.
    concept: str | None = Field(None, max_length=60)


class InvoiceItemUpdate(InvoiceItemSatFields):
    concept: str | None = Field(None, max_length=60)
    description: str | None = Field(None, max_length=255)
    quantity: Decimal | None = Field(None, ge=0, max_digits=12, decimal_places=2)
    unit_amount: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=2)


class InvoiceItemResponse(InvoiceItemBase):
    model_config = ConfigDict(from_attributes=True)

    id: int
    invoice_id: int
    tenant_id: int
    company_id: int

    @computed_field
    @property
    def line_total(self) -> Decimal:
        return (self.quantity or Decimal(0)) * (self.unit_amount or Decimal(0))


class PaymentBase(BaseModel):
    amount: Decimal = Field(..., gt=0, max_digits=14, decimal_places=2)
    payment_date: date | None = None
    method: str | None = Field(None, max_length=40)
    reference: str | None = Field(None, max_length=120)
    notes: str | None = None


class PaymentCreate(PaymentBase):
    invoice_id: int


class PaymentResponse(PaymentBase):
    model_config = ConfigDict(from_attributes=True)

    id: int
    invoice_id: int
    tenant_id: int
    company_id: int
    created_at: datetime


class InvoiceBase(BaseModel):
    reference: str | None = Field(None, max_length=40)
    shipment_id: int | None = None
    quote_id: int | None = None
    account_id: int | None = None
    currency: str = Field("MXN", max_length=3)
    issue_date: date | None = None
    due_date: date | None = None
    tax_rate: Decimal = Field(Decimal(0), ge=0, le=100, max_digits=5, decimal_places=2)
    bank_info: str | None = None
    notes: str | None = None
    owner_user_id: str | None = Field(None, max_length=64)
    # ----- Claves fiscales del CFDI (opcionales mientras no se timbre) -----
    voucher_type_id: int | None = None
    payment_form_id: int | None = None
    payment_method_id: int | None = None
    expedition_zip_code: str | None = Field(None, max_length=5)
    # Modo de timbrado de ESTA factura. 'produccion' emite un CFDI con validez fiscal real
    # ante el SAT; por eso el default es 'pruebas' y subirlo es una decisión explícita.
    stamping_mode: Literal["pruebas", "produccion"] = "pruebas"


class InvoiceCreate(InvoiceBase):
    pass


class InvoiceUpdate(BaseModel):
    reference: str | None = Field(None, max_length=40)
    shipment_id: int | None = None
    quote_id: int | None = None
    account_id: int | None = None
    currency: str | None = Field(None, max_length=3)
    issue_date: date | None = None
    due_date: date | None = None
    tax_rate: Decimal | None = Field(None, ge=0, le=100, max_digits=5, decimal_places=2)
    bank_info: str | None = None
    notes: str | None = None
    owner_user_id: str | None = Field(None, max_length=64)
    voucher_type_id: int | None = None
    payment_form_id: int | None = None
    payment_method_id: int | None = None
    expedition_zip_code: str | None = Field(None, max_length=5)
    stamping_mode: Literal["pruebas", "produccion"] | None = None


class InvoiceResponse(InvoiceBase):
    model_config = ConfigDict(from_attributes=True)

    id: int
    case_id: int | None = None
    status: str
    subtotal: Decimal
    tax_amount: Decimal
    total: Decimal
    paid_amount: Decimal
    balance: Decimal
    ops_cost_total: Decimal | None = None
    sent_at: datetime | None = None
    paid_at: datetime | None = None
    pdf_file_key: str | None = None
    client_reviewed_at: datetime | None = None
    client_approved: bool | None = None
    review_notes: str | None = None
    created_by: str | None = None
    updated_by: str | None = None
    tenant_id: int
    company_id: int
    created_at: datetime
    updated_at: datetime


class InvoiceItemTaxInput(BaseModel):
    """Alta o ajuste de un impuesto de la partida.

    El importe no se recibe: se calcula de la base de la partida por la tasa, para que no
    pueda quedar un desglose que no cuadre con el importe del concepto.
    """

    tax_id: int
    rate: Decimal = Field(..., ge=0, le=1, max_digits=8, decimal_places=6)
    is_withholding: bool = False


class InvoiceItemTaxResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    invoice_item_id: int
    tax_id: int
    is_withholding: bool
    rate: Decimal | None = None
    amount: Decimal
