"""客户登录账号（F41，管理员）与 Client product（F42，隐藏菜单，管理员）。

客户账号只供外部门户（不在本次范围），密码用新哈希格式，不兼容旧门户（文档 12.3 第 8 条）。
"""
import datetime as dt
import uuid

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, NaiveDatetime
from sqlalchemy import delete, func, or_, select
from sqlalchemy.orm import Session

from flyway.api.deps import current_actor, get_db, require_admin_actor
from flyway.api.lists import IdsIn, Page, like, page_params, paginate
from flyway.auth.passwords import hash_password
from flyway.domain.actor import Actor
from flyway.domain.clock import NZ
from flyway.domain.errors import BusinessError
from flyway.models import (
    Client, ClientAccount, ClientAccountClaim, ClientAccountLogin, ClientAccountRole, ClientProduct, ClientRole,
    Product, ProductBatch, Transfer, TransferItem,
)
from flyway.services.audit import record
from flyway.services.common import norm

router = APIRouter(prefix="/api", dependencies=[Depends(require_admin_actor)])
CLIENT_ROLE = "Clients"


class AccountIn(BaseModel):
    client_id: uuid.UUID
    username: str
    password: str = ""  # 编辑时留空表示不改


class AccountsDeleteIn(BaseModel):
    ids: list[str]


def _account_out(a: ClientAccount, names: dict) -> dict:
    return {"id": a.id, "username": a.user_name, "client_id": a.client_id, "client_name": names.get(a.client_id)}


def _names(db: Session, ids) -> dict:
    ids = {i for i in ids if i}
    return {c.id: f"{c.name} (E:{c.email or ''},M:{c.mobile_phone or ''})" for c in db.scalars(select(Client).where(Client.id.in_(ids)))} if ids else {}


@router.get("/client-accounts")
def list_accounts(keyword: str | None = None, client_id: uuid.UUID | None = None, page: Page = Depends(page_params),
                  db: Session = Depends(get_db)):
    q = select(ClientAccount).outerjoin(Client, Client.id == ClientAccount.client_id)
    if keyword:
        q = q.where(or_(like(ClientAccount.user_name, keyword), like(Client.name, keyword)))
    if client_id:
        q = q.where(ClientAccount.client_id == client_id)
    rows, total = paginate(db, q, page, {"username": ClientAccount.user_name, "client": Client.name}, "username")
    names = _names(db, [a.client_id for a in rows])
    return {"items": [_account_out(a, names) for a in rows], "total": total}


def _ensure_role(db: Session, account_id: str) -> None:
    role = db.scalar(select(ClientRole).where(ClientRole.name == CLIENT_ROLE))
    if role is None:
        role = ClientRole(id=str(uuid.uuid4()), name=CLIENT_ROLE)
        db.add(role)
        db.flush()
    if db.get(ClientAccountRole, (account_id, role.id)) is None:
        db.add(ClientAccountRole(user_id=account_id, role_id=role.id))


def save_account(db: Session, body: AccountIn, account_id: str | None = None) -> ClientAccount:
    username = norm(body.username)
    if not username:
        raise BusinessError('"User name" is required.', "username")
    if db.get(Client, body.client_id) is None:
        raise BusinessError("Client not found.", "client_id")
    dup = select(ClientAccount.id).where(func.lower(ClientAccount.user_name) == username.lower())
    if account_id:
        dup = dup.where(ClientAccount.id != account_id)
    if db.scalar(dup.limit(1)):
        raise BusinessError(f'Name {username} is already taken.', "username")
    if account_id:
        a = db.get(ClientAccount, account_id)
        if a is None:
            raise HTTPException(status_code=404)
    else:
        if not body.password:
            raise BusinessError('"Password" is required.', "password")
        a = ClientAccount(id=str(uuid.uuid4()), discriminator="ClientAccount")
        db.add(a)
    a.user_name, a.client_id = username, body.client_id
    if body.password:
        a.password_hash = hash_password(body.password)
        a.security_stamp = str(uuid.uuid4())
    db.flush()
    _ensure_role(db, a.id)
    db.flush()
    return a


@router.post("/client-accounts")
def create_account(body: AccountIn, db: Session = Depends(get_db), actor: Actor = Depends(current_actor)):
    a = save_account(db, body)
    record(db, actor, "create", "client_account", a.id, a.user_name)
    db.commit()
    return _account_out(a, _names(db, [a.client_id]))


@router.get("/client-accounts/{account_id}")
def get_account(account_id: str, db: Session = Depends(get_db)):
    account = db.get(ClientAccount, account_id)
    if account is None:
        raise HTTPException(status_code=404)
    return _account_out(account, _names(db, [account.client_id]))


@router.put("/client-accounts/{account_id}")
def update_account(account_id: str, body: AccountIn, db: Session = Depends(get_db),
                   actor: Actor = Depends(current_actor)):
    a = save_account(db, body, account_id)
    record(db, actor, "edit", "client_account", a.id, a.user_name)
    db.commit()
    return _account_out(a, _names(db, [a.client_id]))


@router.post("/client-accounts/delete", status_code=204)
def delete_accounts(body: AccountsDeleteIn, db: Session = Depends(get_db), actor: Actor = Depends(current_actor)):
    for model, col in ((ClientAccountRole, ClientAccountRole.user_id), (ClientAccountClaim, ClientAccountClaim.user_id),
                       (ClientAccountLogin, ClientAccountLogin.user_id)):
        db.execute(delete(model).where(col.in_(body.ids)))
    for a in db.scalars(select(ClientAccount).where(ClientAccount.id.in_(body.ids))).all():
        record(db, actor, "delete", "client_account", a.id, a.user_name or "")
        db.delete(a)
    db.commit()


# ---- Client product（F42：菜单继续隐藏，页面与接口保留）----
class ClientProductIn(BaseModel):
    client_id: uuid.UUID
    product_id: uuid.UUID
    opening_stock: int = 0
    opening_date: dt.date
    opening_display_date: NaiveDatetime | None = None
    closing_stock: int = 0
    closing_date: dt.date | None = None


def _cp_out(cp: ClientProduct, clients: dict, products: dict) -> dict:
    return {"id": cp.id, "client_id": cp.client_id, "client_name": clients.get(cp.client_id),
            "product_id": cp.product_id, "product_title": products.get(cp.product_id),
            "opening_stock": cp.opening_stock, "opening_date": cp.opening_date,
            "opening_display_date": cp.opening_display_date or dt.datetime.combine(cp.opening_date, dt.time()),
            "closing_display_date": cp.closing_display_date or dt.datetime.combine(cp.closing_date, dt.time()),
            "closing_stock": cp.closing_stock, "closing_date": cp.closing_date}


def _cp_rows(db: Session, rows) -> list[dict]:
    clients = _names(db, [r.client_id for r in rows])
    pids = {r.product_id for r in rows}
    products = dict(db.execute(select(Product.id, Product.title).where(Product.id.in_(pids))).all()) if pids else {}
    return [_cp_out(r, clients, products) for r in rows]


@router.get("/client-products")
def list_client_products(client_id: uuid.UUID | None = None, page: Page = Depends(page_params),
                         db: Session = Depends(get_db)):
    q = select(ClientProduct)
    if client_id:
        q = q.where(ClientProduct.client_id == client_id)
    rows, total = paginate(db, q, page, {"opening_date": ClientProduct.opening_date}, "-opening_date")
    return {"items": _cp_rows(db, rows), "total": total}


def _save_cp(db: Session, body: ClientProductIn, cp: ClientProduct) -> dict:
    if db.get(Client, body.client_id) is None or db.get(Product, body.product_id) is None:
        raise BusinessError("Client or product not found.")
    if body.opening_display_date and body.opening_display_date.date() != body.opening_date:
        raise BusinessError('Opening date and time must have the same date.', 'opening_date')
    if not body.opening_display_date and cp.opening_date != body.opening_date:
        cp.opening_display_date = dt.datetime.combine(body.opening_date, dt.time())
    for k, v in body.model_dump(exclude_unset=True).items():
        setattr(cp, k, v)
    if cp.closing_date is None:
        cp.closing_date = body.opening_date
    if 'closing_stock' not in body.model_fields_set and 'closing_date' not in body.model_fields_set:
        remaining = db.scalar(select(func.coalesce(func.sum(TransferItem.remaining_stock), 0))
                              .join(Transfer).join(ProductBatch).where(Transfer.client_id == body.client_id,
                                  Transfer.is_load_out.is_(False), ProductBatch.product_id == body.product_id,
                                  TransferItem.related_transfer_item_id.is_(None)))
        cp.closing_stock = (cp.opening_stock or 0) + remaining
        last = db.scalar(select(func.max(Transfer.date)).join(TransferItem).join(ProductBatch)
                         .where(Transfer.client_id == body.client_id, ProductBatch.product_id == body.product_id))
        cp.closing_date = last.date() if last else body.opening_date
        cp.closing_display_date = last.astimezone(NZ).replace(tzinfo=None) if last else cp.opening_display_date
    db.add(cp)
    db.commit()
    return _cp_rows(db, [cp])[0]


@router.post("/client-products")
def create_client_product(body: ClientProductIn, db: Session = Depends(get_db)):
    return _save_cp(db, body, ClientProduct(id=uuid.uuid4()))


@router.get("/client-products/{cp_id}")
def get_client_product(cp_id: uuid.UUID, db: Session = Depends(get_db)):
    product = db.get(ClientProduct, cp_id)
    if product is None:
        raise HTTPException(status_code=404)
    return _cp_rows(db, [product])[0]


@router.put("/client-products/{cp_id}")
def update_client_product(cp_id: uuid.UUID, body: ClientProductIn, db: Session = Depends(get_db)):
    cp = db.get(ClientProduct, cp_id)
    if cp is None:
        raise HTTPException(status_code=404)
    return _save_cp(db, body, cp)


@router.post("/client-products/delete", status_code=204)
def delete_client_products(body: IdsIn, db: Session = Depends(get_db)):
    db.execute(delete(ClientProduct).where(ClientProduct.id.in_(body.ids)))
    db.commit()
