"""PB (Permintaan Barang) service."""

from datetime import datetime, timezone
from decimal import Decimal

from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.pb import StatusPB, StatusPBItem
from app.models.role import AksiEnum, ModulEnum
from app.repositories import pb_repository, role_repository
from app.schemas.pb import (
    PBCreate, PBUpdate, PBResponse, PBItemResponse,
    ProjectCreate, ProjectUpdate, ProjectResponse,
    PBLedgerResponse, LedgerEntry,
)
from app.utils.file_upload import get_spbu_code


# ── Permission helpers ────────────────────────────────────────────────────────

async def _can(db, user, spbu_id: int, aksi: AksiEnum) -> bool:
    if user.is_superadmin:
        return True
    assignment = next((a for a in (user.assignments or []) if a.spbu_id == spbu_id), None)
    if not assignment:
        return False
    return await role_repository.has_permission(db, assignment.role_id, ModulEnum.pb, aksi)


def _require(cond: bool, msg: str) -> None:
    if not cond:
        raise PermissionError(msg)


# ── Response builders ─────────────────────────────────────────────────────────

def _build_pb_response(pb) -> PBResponse:
    items = [PBItemResponse.from_orm_item(i) for i in (pb.items or [])]
    total_estimasi = sum(i.total_estimasi for i in items)
    total_approved = sum(
        (i.qty_approved or Decimal(0)) * i.harga_satuan
        for i in items
        if i.status == StatusPBItem.APPROVED
    )
    return PBResponse(
        id=pb.id,
        spbu_id=pb.spbu_id,
        nomor=pb.nomor,
        judul=pb.judul,
        deskripsi=pb.deskripsi,
        project_id=pb.project_id,
        project_nama=pb.project.nama if pb.project else None,
        is_kas_kecil=pb.is_kas_kecil,
        status=pb.status if isinstance(pb.status, str) else pb.status.value,
        attachment_url=pb.attachment_url,
        total_estimasi=total_estimasi,
        total_approved=total_approved,
        total_realisasi=Decimal(0),  # filled in get_pb_with_ledger
        created_by_name=pb.created_by.name if pb.created_by else None,
        submitted_at=pb.submitted_at,
        forwarded_by_name=pb.forwarded_by.name if pb.forwarded_by else None,
        forwarded_at=pb.forwarded_at,
        recalled_by_name=pb.recalled_by.name if pb.recalled_by else None,
        recalled_at=pb.recalled_at,
        reviewed_by_name=pb.reviewed_by.name if pb.reviewed_by else None,
        reviewed_at=pb.reviewed_at,
        catatan_head=pb.catatan_head,
        created_at=pb.created_at,
        items=items,
    )


# ── Project ───────────────────────────────────────────────────────────────────

async def list_projects(db: AsyncSession, spbu_id: int) -> list[ProjectResponse]:
    rows = await pb_repository.get_projects(db, spbu_id)
    return [ProjectResponse.model_validate(r) for r in rows]


async def create_project(db: AsyncSession, spbu_id: int, body: ProjectCreate) -> ProjectResponse:
    try:
        proj = await pb_repository.create_project(db, spbu_id, body.nama, body.deskripsi)
        await db.commit()
        return ProjectResponse.model_validate(proj)
    except SQLAlchemyError:
        await db.rollback()
        raise


async def update_project(db: AsyncSession, spbu_id: int, project_id: int, body: ProjectUpdate) -> ProjectResponse:
    proj = await pb_repository.get_project_by_id(db, project_id, spbu_id)
    if proj is None:
        raise ValueError("Project tidak ditemukan")
    data = {k: v for k, v in body.model_dump().items() if v is not None}
    try:
        proj = await pb_repository.update_project(db, proj, data)
        await db.commit()
        return ProjectResponse.model_validate(proj)
    except SQLAlchemyError:
        await db.rollback()
        raise


# ── PB CRUD ───────────────────────────────────────────────────────────────────

async def list_pbs(
    db: AsyncSession, spbu_id: int,
    status: str | None, project_id: int | None,
    skip: int, limit: int,
) -> tuple[list[PBResponse], int]:
    rows, total = await pb_repository.get_pb_list(db, spbu_id, status, project_id, skip, limit)
    return [_build_pb_response(r) for r in rows], total


async def get_pb(db: AsyncSession, spbu_id: int, pb_id: int) -> PBResponse:
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    resp = _build_pb_response(pb)
    # Fill realisasi from approved expenses
    expenses = await pb_repository.get_expenses_for_pb(db, pb_id)
    resp.total_realisasi = sum(Decimal(str(e.jumlah)) for e in expenses)
    return resp


async def create_pb(db: AsyncSession, spbu_id: int, user_id: int, body: PBCreate) -> PBResponse:
    if body.project_id:
        proj = await pb_repository.get_project_by_id(db, body.project_id, spbu_id)
        if proj is None:
            raise ValueError("Project tidak ditemukan")
        if proj.is_closed:
            raise ValueError("Project sudah closed, tidak bisa menambah PB baru")

    spbu_code = await get_spbu_code(db, spbu_id)
    now = datetime.now(timezone.utc)
    yyyymm = now.strftime("%Y%m")
    nomor = await pb_repository.next_pb_nomor(db, spbu_code, yyyymm)

    data = body.model_dump()
    data["items"] = [i.model_dump() for i in body.items]

    try:
        pb = await pb_repository.create_pb(db, spbu_id, user_id, nomor, data)
        await db.commit()
    except IntegrityError:
        await db.rollback()
        raise ValueError("Duplikat nomor PB, coba lagi")
    except SQLAlchemyError:
        await db.rollback()
        raise

    pb = await pb_repository.reload_pb(db, pb.id)
    return _build_pb_response(pb)


async def update_pb(db: AsyncSession, spbu_id: int, pb_id: int, body: PBUpdate) -> PBResponse:
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    if pb.status not in (StatusPB.DRAFT, StatusPB.SUBMITTED):
        raise ValueError("PB hanya bisa diedit saat Draft atau Submitted")

    if body.project_id:
        proj = await pb_repository.get_project_by_id(db, body.project_id, spbu_id)
        if proj is None:
            raise ValueError("Project tidak ditemukan")
        if proj.is_closed:
            raise ValueError("Project sudah closed")

    data = {k: v for k, v in body.model_dump().items() if v is not None}
    if "items" in data and data["items"] is not None:
        data["items"] = [i.model_dump() if hasattr(i, "model_dump") else i for i in data["items"]]

    try:
        pb = await pb_repository.update_pb(db, pb, data)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise

    pb = await pb_repository.reload_pb(db, pb.id)
    return _build_pb_response(pb)


async def delete_pb(db: AsyncSession, spbu_id: int, pb_id: int) -> None:
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    if pb.status != StatusPB.DRAFT:
        raise ValueError("Hanya PB Draft yang bisa dihapus")
    attachment_url = pb.attachment_url
    try:
        await pb_repository.delete_pb(db, pb)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    if attachment_url:
        from app.utils.file_upload import delete_file
        await delete_file(attachment_url)


# ── Status transitions ────────────────────────────────────────────────────────

async def submit_pb(db: AsyncSession, spbu_id: int, pb_id: int, user_id: int) -> PBResponse:
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    if pb.status != StatusPB.DRAFT:
        raise ValueError("Hanya PB Draft yang bisa di-submit")
    if not pb.items:
        raise ValueError("PB harus punya minimal 1 item sebelum submit")
    for item in pb.items:
        if not item.nama_item or item.qty_requested <= 0 or item.harga_satuan <= 0:
            raise ValueError(f"Item '{item.nama_item or '?'}': nama, qty, dan harga wajib diisi dan > 0")

    try:
        pb.status = StatusPB.SUBMITTED
        pb.submitted_at = datetime.now(timezone.utc)
        from app.utils.audit import log_action
        await log_action(db, user_id=user_id, spbu_id=spbu_id, aksi="submit", modul="pb", object_id=pb_id)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_pb_response(await pb_repository.reload_pb(db, pb_id))


async def recall_pb(db: AsyncSession, spbu_id: int, pb_id: int, user_id: int) -> PBResponse:
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    if pb.status != StatusPB.SUBMITTED:
        raise ValueError("Recall hanya bisa pada status Submitted")
    try:
        pb.status = StatusPB.DRAFT
        pb.recalled_by_id = user_id
        pb.recalled_at = datetime.now(timezone.utc)
        from app.utils.audit import log_action
        await log_action(db, user_id=user_id, spbu_id=spbu_id, aksi="recall", modul="pb", object_id=pb_id)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_pb_response(await pb_repository.reload_pb(db, pb_id))


async def forward_pb(db: AsyncSession, spbu_id: int, pb_id: int, user, body) -> PBResponse:
    """Manager edits items (optional) then sends for Head approval."""
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    if pb.status != StatusPB.SUBMITTED:
        raise ValueError("Forward hanya bisa pada status Submitted")
    _require(await _can(db, user, spbu_id, AksiEnum.forward), "Tidak ada izin untuk forward PB")

    # Apply any item edits from manager
    if body.items is not None:
        data = {"items": [i.model_dump() if hasattr(i, "model_dump") else i for i in body.items]}
        pb = await pb_repository.update_pb(db, pb, data)

    try:
        pb.status = StatusPB.FOR_APPROVAL
        pb.forwarded_by_id = user.id
        pb.forwarded_at = datetime.now(timezone.utc)
        from app.utils.audit import log_action
        await log_action(db, user_id=user.id, spbu_id=spbu_id, aksi="forward", modul="pb", object_id=pb_id)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_pb_response(await pb_repository.reload_pb(db, pb_id))


async def pull_back_pb(db: AsyncSession, spbu_id: int, pb_id: int, user) -> PBResponse:
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    if pb.status != StatusPB.FOR_APPROVAL:
        raise ValueError("Pull Back hanya bisa pada status For Approval")
    _require(await _can(db, user, spbu_id, AksiEnum.forward), "Tidak ada izin untuk pull back PB")
    try:
        pb.status = StatusPB.SUBMITTED
        from app.utils.audit import log_action
        await log_action(db, user_id=user.id, spbu_id=spbu_id, aksi="pull_back", modul="pb", object_id=pb_id)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_pb_response(await pb_repository.reload_pb(db, pb_id))


async def review_pb(db: AsyncSession, spbu_id: int, pb_id: int, user, body) -> PBResponse:
    """Head of Management approves/rejects per line, sets overall status."""
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    if pb.status != StatusPB.FOR_APPROVAL:
        raise ValueError("Review hanya bisa pada status For Approval")
    _require(await _can(db, user, spbu_id, AksiEnum.approve), "Tidak ada izin untuk approve PB")

    # Update each line item
    item_map = {i.id: i for i in pb.items}
    for line in body.items:
        item = item_map.get(line["id"])
        if item is None:
            raise ValueError(f"Item id {line['id']} tidak ditemukan")
        item_data = {
            "status": line["status"],
            "qty_approved": line.get("qty_approved"),
            "catatan_head": line.get("catatan_head"),
        }
        await pb_repository.update_pb_item(db, item, {k: v for k, v in item_data.items() if v is not None})
        if line["status"] == "approved" and item_data["qty_approved"] is None:
            raise ValueError(f"qty_approved wajib saat approve item '{item.nama_item}'")

    # Determine overall PB status: approved if any line approved, else rejected
    all_statuses = {line["status"] for line in body.items}
    pb_status = StatusPB.APPROVED if "approved" in all_statuses else StatusPB.REJECTED

    try:
        pb.status = pb_status
        pb.reviewed_by_id = user.id
        pb.reviewed_at = datetime.now(timezone.utc)
        pb.catatan_head = body.catatan_head
        from app.utils.audit import log_action
        aksi = "approve" if pb_status == StatusPB.APPROVED else "reject"
        await log_action(db, user_id=user.id, spbu_id=spbu_id, aksi=aksi, modul="pb", object_id=pb_id,
                         detail={"catatan": body.catatan_head} if body.catatan_head else None)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_pb_response(await pb_repository.reload_pb(db, pb_id))


# ── Upload attachment ─────────────────────────────────────────────────────────

async def upload_attachment(
    db: AsyncSession, spbu_id: int, pb_id: int,
    file_bytes: bytes, filename: str,
) -> PBResponse:
    from app.utils.file_upload import save_upload, delete_file, UploadContext
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")
    if pb.status == StatusPB.APPROVED:
        raise ValueError("Attachment tidak bisa diupload setelah approved")

    old_url = pb.attachment_url
    spbu_code = await get_spbu_code(db, spbu_id)
    from datetime import date
    ctx = UploadContext(spbu_code, "pb", date.today())
    url = await save_upload(file_bytes, filename, ctx)

    try:
        pb.attachment_url = url
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    if old_url:
        await delete_file(old_url)
    return _build_pb_response(await pb_repository.reload_pb(db, pb_id))


# ── Ledger ────────────────────────────────────────────────────────────────────

async def get_pb_ledger(db: AsyncSession, spbu_id: int, pb_id: int) -> PBLedgerResponse:
    pb = await pb_repository.get_pb_by_id(db, pb_id, spbu_id)
    if pb is None:
        raise ValueError("PB tidak ditemukan")

    resp = _build_pb_response(pb)
    expenses = await pb_repository.get_expenses_for_pb(db, pb_id)

    entries: list[LedgerEntry] = []
    saldo = Decimal(0)

    if pb.status == StatusPB.APPROVED and pb.reviewed_at:
        kredit = resp.total_approved
        saldo += kredit
        entries.append(LedgerEntry(
            tanggal=pb.reviewed_at.strftime("%Y-%m-%d"),
            keterangan=f"Alokasi PB disetujui — {pb.judul}",
            debit=Decimal(0),
            kredit=kredit,
            saldo=saldo,
            ref_type="alokasi",
            ref_id=pb.id,
        ))

    for exp in expenses:
        debit = Decimal(str(exp.jumlah))
        saldo -= debit
        entries.append(LedgerEntry(
            tanggal=str(exp.tanggal),
            keterangan=exp.keterangan or f"Expense #{exp.id}",
            debit=debit,
            kredit=Decimal(0),
            saldo=saldo,
            ref_type="expense",
            ref_id=exp.id,
        ))

    total_realisasi = sum(Decimal(str(e.jumlah)) for e in expenses)
    return PBLedgerResponse(
        pb_id=pb.id,
        nomor=pb.nomor,
        judul=pb.judul,
        total_approved=resp.total_approved,
        total_realisasi=total_realisasi,
        sisa=resp.total_approved - total_realisasi,
        entries=entries,
    )
