"""Expense service."""

from datetime import date, datetime, timezone
from decimal import Decimal

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

from app.models.expenses import StatusExpense
from app.models.role import AksiEnum, ModulEnum
from app.repositories import expense_repository, role_repository
from app.schemas.expenses import (
    ExpenseCreate,
    ExpenseUpdate,
    ExpenseResponse,
    ExpenseKategoriResponse,
)
from app.utils.file_upload import save_upload, UploadContext


async def _user_can_approve_expense(db, user, spbu_id: int) -> bool:
    """Return True if user has expenses:approve permission for this SPBU."""
    if user.is_superadmin:
        return True
    assignment = next((a for a in (user.assignments or []) if a.spbu_id == spbu_id), None)
    if assignment is None:
        return False
    return await role_repository.has_permission(
        db, assignment.role_id, ModulEnum.expenses, AksiEnum.approve
    )


def _build_response(expense) -> ExpenseResponse:
    return ExpenseResponse(
        id=expense.id,
        spbu_id=expense.spbu_id,
        laporan_shift_id=expense.laporan_shift_id,
        tanggal=expense.tanggal,
        kategori_id=expense.kategori_id,
        kategori_nama=expense.kategori.nama if expense.kategori else "",
        keterangan=expense.keterangan,
        jumlah=expense.jumlah,
        bukti_url=expense.bukti_url,
        status=expense.status if isinstance(expense.status, str) else expense.status.value,
        created_by_name=expense.created_by.name if expense.created_by else None,
        submitted_by_name=expense.submitted_by.name if expense.submitted_by else None,
        submitted_at=expense.submitted_at,
        reviewed_by_name=expense.reviewed_by.name if expense.reviewed_by else None,
        reviewed_at=expense.reviewed_at,
        catatan_review=expense.catatan_review,
        recalled_by_name=expense.recalled_by.name if expense.recalled_by else None,
        recalled_at=expense.recalled_at,
        unlocked_by_name=expense.unlocked_by.name if expense.unlocked_by else None,
        unlocked_at=expense.unlocked_at,
        unlock_reason=expense.unlock_reason,
        created_at=expense.created_at,
    )


async def list_kategori(db: AsyncSession, spbu_id: int) -> list[ExpenseKategoriResponse]:
    rows = await expense_repository.get_kategori_list(db, spbu_id)
    return [ExpenseKategoriResponse.model_validate(r) for r in rows]


async def create_kategori(
    db: AsyncSession, spbu_id: int, nama: str, urutan: int
) -> ExpenseKategoriResponse:
    try:
        kat = await expense_repository.create_kategori(db, spbu_id, nama, urutan)
        await db.commit()
        return ExpenseKategoriResponse.model_validate(kat)
    except IntegrityError:
        await db.rollback()
        raise ValueError("Duplikat atau constraint error pada kategori")
    except SQLAlchemyError:
        await db.rollback()
        raise


async def list_expenses(
    db: AsyncSession,
    spbu_id: int,
    tanggal: date | None = None,
    tanggal_from: date | None = None,
    tanggal_to: date | None = None,
    laporan_shift_id: int | None = None,
    kategori_id: int | None = None,
    skip: int = 0,
    limit: int = 50,
) -> tuple[list[ExpenseResponse], int]:
    rows, total = await expense_repository.get_all_expenses(
        db, spbu_id, tanggal, tanggal_from, tanggal_to, laporan_shift_id, kategori_id, skip, limit
    )
    return [_build_response(r) for r in rows], total


async def get_expense(db: AsyncSession, spbu_id: int, expense_id: int) -> ExpenseResponse:
    expense = await expense_repository.get_expense_by_id(db, expense_id, spbu_id)
    if expense is None:
        raise ValueError("Expense tidak ditemukan")
    return _build_response(expense)


async def create_expense(
    db: AsyncSession, spbu_id: int, user_id: int, body: ExpenseCreate
) -> ExpenseResponse:
    # Validate kategori belongs to global or this spbu
    katlist = await expense_repository.get_kategori_list(db, spbu_id)
    kat_ids = {k.id for k in katlist}
    if body.kategori_id not in kat_ids:
        raise ValueError("Kategori tidak valid")
    data = body.model_dump()
    try:
        expense = await expense_repository.create_expense(db, spbu_id, user_id, data)
        await db.commit()
        return _build_response(expense)
    except IntegrityError:
        await db.rollback()
        raise ValueError("Duplikat atau constraint error pada expense")
    except SQLAlchemyError:
        await db.rollback()
        raise


async def update_expense(
    db: AsyncSession, spbu_id: int, expense_id: int, body: ExpenseUpdate
) -> ExpenseResponse:
    expense = await expense_repository.get_expense_by_id(db, expense_id, spbu_id)
    if expense is None:
        raise ValueError("Expense tidak ditemukan")
    if expense.status == StatusExpense.APPROVED:
        raise ValueError("Expense yang sudah Approved tidak bisa diedit. Unlock terlebih dahulu.")
    data = {k: v for k, v in body.model_dump().items() if v is not None}
    try:
        expense = await expense_repository.update_expense(db, expense, data)
        await db.commit()
        return _build_response(expense)
    except IntegrityError:
        await db.rollback()
        raise ValueError("Duplikat atau constraint error pada expense")
    except SQLAlchemyError:
        await db.rollback()
        raise


async def delete_expense(db: AsyncSession, spbu_id: int, expense_id: int) -> None:
    from app.utils.file_upload import delete_file
    expense = await expense_repository.get_expense_by_id(db, expense_id, spbu_id)
    if expense is None:
        raise ValueError("Expense tidak ditemukan")
    if expense.status != StatusExpense.DRAFT:
        raise ValueError("Hanya expense berstatus DRAFT yang bisa dihapus")
    bukti_url = expense.bukti_url
    try:
        await expense_repository.delete_expense(db, expense)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    await delete_file(bukti_url)


async def submit_expense(db: AsyncSession, spbu_id: int, expense_id: int, user_id: int) -> ExpenseResponse:
    expense = await expense_repository.get_expense_by_id(db, expense_id, spbu_id)
    if expense is None:
        raise ValueError("Expense tidak ditemukan")
    if expense.status != StatusExpense.DRAFT:
        raise ValueError("Hanya expense DRAFT yang bisa di-submit")
    try:
        expense.status = StatusExpense.SUBMITTED
        expense.submitted_by_id = user_id
        expense.submitted_at = datetime.now(timezone.utc)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_response(await expense_repository.get_expense_by_id(db, expense_id, spbu_id))


async def recall_expense(db: AsyncSession, spbu_id: int, expense_id: int, user_id: int) -> ExpenseResponse:
    expense = await expense_repository.get_expense_by_id(db, expense_id, spbu_id)
    if expense is None:
        raise ValueError("Expense tidak ditemukan")
    if expense.status != StatusExpense.SUBMITTED:
        raise ValueError("Recall hanya bisa dilakukan pada expense SUBMITTED")
    try:
        expense.status = StatusExpense.DRAFT
        expense.recalled_by_id = user_id
        expense.recalled_at = datetime.now(timezone.utc)
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_response(await expense_repository.get_expense_by_id(db, expense_id, spbu_id))


async def review_expense(
    db: AsyncSession, spbu_id: int, expense_id: int, user, action: str, catatan: str | None
) -> ExpenseResponse:
    expense = await expense_repository.get_expense_by_id(db, expense_id, spbu_id)
    if expense is None:
        raise ValueError("Expense tidak ditemukan")
    if not await _user_can_approve_expense(db, user, spbu_id):
        raise PermissionError("Tidak ada izin untuk approve expense")
    if expense.status != StatusExpense.SUBMITTED:
        raise ValueError("Hanya expense SUBMITTED yang bisa di-review")
    new_status = StatusExpense.APPROVED if action == "approve" else StatusExpense.REJECTED
    try:
        expense.status = new_status
        expense.reviewed_by_id = user.id
        expense.reviewed_at = datetime.now(timezone.utc)
        expense.catatan_review = catatan
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_response(await expense_repository.get_expense_by_id(db, expense_id, spbu_id))


async def unlock_expense(
    db: AsyncSession, spbu_id: int, expense_id: int, user, alasan: str
) -> ExpenseResponse:
    expense = await expense_repository.get_expense_by_id(db, expense_id, spbu_id)
    if expense is None:
        raise ValueError("Expense tidak ditemukan")
    if not await _user_can_approve_expense(db, user, spbu_id):
        raise PermissionError("Tidak ada izin untuk unlock expense")
    if expense.status != StatusExpense.APPROVED:
        raise ValueError("Hanya expense APPROVED yang bisa di-unlock")
    if not alasan or not alasan.strip():
        raise ValueError("Alasan unlock wajib diisi")
    try:
        expense.status = StatusExpense.DRAFT
        expense.unlocked_by_id = user.id
        expense.unlocked_at = datetime.now(timezone.utc)
        expense.unlock_reason = alasan
        await db.commit()
    except SQLAlchemyError:
        await db.rollback()
        raise
    return _build_response(await expense_repository.get_expense_by_id(db, expense_id, spbu_id))


async def upload_bukti(
    db: AsyncSession,
    spbu_id: int,
    expense_id: int,
    file_bytes: bytes,
    filename: str,
) -> ExpenseResponse:
    from app.utils.file_upload import delete_file
    expense = await expense_repository.get_expense_by_id(db, expense_id, spbu_id)
    if expense is None:
        raise ValueError("Expense tidak ditemukan")
    if expense.status == StatusExpense.APPROVED:
        raise ValueError("Bukti tidak bisa diupload setelah approved")
    old_bukti_url = expense.bukti_url
    from app.utils.file_upload import get_spbu_code
    spbu_code = await get_spbu_code(db, spbu_id)
    ctx = UploadContext(spbu_code, "expenses", expense.tanggal)
    url = await save_upload(file_bytes, filename, ctx)
    try:
        expense = await expense_repository.update_bukti_url(db, expense, url)
        await db.commit()
    except IntegrityError:
        await db.rollback()
        raise ValueError("Duplikat atau constraint error")
    except SQLAlchemyError:
        await db.rollback()
        raise
    await delete_file(old_bukti_url)
    return _build_response(expense)
