import datetime as dt
import uuid
from dataclasses import dataclass, field, fields

from sqlalchemy import delete, select
from sqlalchemy.orm import Session

from flyway.domain.actor import Actor
from flyway.domain.clock import require_aware
from flyway.domain.errors import BusinessError
from flyway.domain.units import parse_units, pick_unit_for_remaining
from flyway.models import Transfer, TransferItem, TransferRelation
from flyway.services.audit import record
from flyway.services.common import (
    ensure_supplier, ensure_unique_code, get_transfer, require_admin, set_load_in_links,
)
from flyway.services.stock import compute_remaining, lock_items, refresh


@dataclass
class LoadOutHeader:
    client_id: uuid.UUID
    code: str
    date: dt.datetime
    to_location: str | None = None
    to_rmp: str | None = None
    ed_identification: str | None = None
    export_to: str | None = None
    transport_company: str | None = None
    vehicle_registration: str | None = None
    condition_of_vehicle: str | None = None
    non_conforming_product: bool = False
    container_number: str | None = None
    seal_number: str | None = None
    can_number: str | None = None
    awb_number: str | None = None
    notes: str | None = None
    load_in_ids: list[uuid.UUID] = field(default_factory=list)


@dataclass
class LoadOutLine:
    source_item_id: uuid.UUID
    quantity: int
    unit: int = 1
    reference: str | None = None
    id: uuid.UUID | None = None


@dataclass
class RemainingPick:
    line: LoadOutLine
    remainder: int


_HEADER_FIELDS = [f.name for f in fields(LoadOutHeader) if f.name != "load_in_ids"]


def check_lines(lines) -> None:
    if len({line.source_item_id for line in lines}) != len(lines):
        raise BusinessError("The same source item appears more than once.", "lines")
    for line in lines:
        if line.quantity < 1 or line.unit < 1:
            raise BusinessError("Quantity and unit must be at least 1.", "quantity")


def check_line_source(src: TransferItem | None, client_id: uuid.UUID) -> TransferItem:
    """来源必须是同一客户、已完成的入库类明细（文档 10.4-1）。"""
    if src is None or src.transfer.is_load_out or src.transfer.is_draft or src.transfer.client_id != client_id:
        raise BusinessError("Not found product.", "lines")
    return src


def available_for(session: Session, src: TransferItem, old: TransferItem | None, counted: bool) -> int:
    """可用量 = 当前剩余 + 本单旧行已扣减的数量（仅当本单旧行已计入扣减时）。"""
    available = compute_remaining(session, src)
    if old is not None and counted and old.related_transfer_item_id == src.id:
        available += old.quantity * old.unit
    return available


def save_load_out(session: Session, actor: Actor, header: LoadOutHeader, lines: list[LoadOutLine],
                  transfer_id: uuid.UUID | None = None) -> Transfer:
    require_aware(header.date, "date")
    check_lines(lines)
    code = ensure_unique_code(session, header.code, [transfer_id])
    if transfer_id:
        transfer = get_transfer(session, transfer_id, load_out=True, local=False)
        action = "edit"
    else:
        transfer = Transfer(id=uuid.uuid4(), is_load_out=True, is_local=False, is_draft=True,
                            operator=actor.full_name)
        action = "create"
    old_items = {i.id: i for i in transfer.items}
    old_sources = [i.related_transfer_item_id for i in old_items.values()]
    # 新单先填好单据头再加入会话，避免后续查询触发的自动写库遇到空的必填列
    for name in _HEADER_FIELDS:
        setattr(transfer, name, getattr(header, name))
    transfer.code, transfer.transfer_from_id = code, None
    session.add(transfer)
    sources = {s.id: s for s in lock_items(session, old_sources + [l.source_item_id for l in lines])}
    set_load_in_links(session, transfer, header.load_in_ids)
    ensure_supplier(session, header.to_location, header.to_rmp)

    for line in lines:
        src = check_line_source(sources.get(line.source_item_id), header.client_id)
        old = old_items.get(line.id) if line.id else None
        available = available_for(session, src, old, counted=not transfer.is_draft)
        if line.quantity * line.unit > available:
            raise BusinessError(f"The Quantity value not more than {available // line.unit}.", "quantity")

    keep = {line.id for line in lines if line.id}
    for item in list(transfer.items):
        if item.id not in keep:
            transfer.items.remove(item)
    # 明细字段填完之前不能自动写库（懒加载会触发 autoflush）
    with session.no_autoflush:
        for line in lines:
            src = sources[line.source_item_id]
            item = old_items.get(line.id) if line.id else None
            if item is None:
                item = TransferItem(id=uuid.uuid4())
                transfer.items.append(item)
            item.main_item, item.batch_id = src, src.batch_id
            item.quantity, item.unit, item.reference = line.quantity, line.unit, line.reference
    session.flush()
    if not transfer.is_draft:
        refresh(session, sources.values())
    record(session, actor, action, "load_out", transfer.id, code)
    return transfer


def set_load_out_draft(session: Session, actor: Actor, transfer_id: uuid.UUID, is_draft: bool) -> Transfer:
    transfer = get_transfer(session, transfer_id, load_out=True, local=False)
    if transfer.is_draft == is_draft:
        return transfer
    sources = lock_items(session, [i.related_transfer_item_id for i in transfer.items])
    transfer.is_draft = is_draft
    refresh(session, sources)
    record(session, actor, "revert" if is_draft else "complete", "load_out", transfer.id, transfer.code)
    return transfer


def delete_load_outs(session: Session, actor: Actor, ids: list[uuid.UUID]) -> None:
    require_admin(actor)
    transfers = session.scalars(select(Transfer).where(
        Transfer.id.in_(ids), Transfer.is_load_out.is_(True), Transfer.is_local.is_(False)
    ).with_for_update().execution_options(populate_existing=True)).all()
    sources = lock_items(session, [i.related_transfer_item_id for t in transfers for i in t.items])
    session.execute(delete(TransferRelation).where(TransferRelation.rel_transfer_id.in_([t.id for t in transfers])))
    for t in transfers:
        record(session, actor, "delete", "load_out", t.id, t.code)
        session.delete(t)
    session.flush()
    refresh(session, sources)


def lines_for_all_remaining(session: Session, load_in_id: uuid.UUID) -> list[RemainingPick]:
    transfer = session.get(Transfer, load_in_id)
    if transfer is None or transfer.is_load_out:
        return []
    picks = []
    for item in transfer.items:
        product = item.batch.product
        picked = pick_unit_for_remaining(item.remaining_stock, product.default_unit,
                                         parse_units(product.available_unit))
        if picked:
            quantity, unit, remainder = picked
            picks.append(RemainingPick(LoadOutLine(item.id, quantity, unit), remainder))
    return picks
