"""Penyetoran service — business logic for the per-shift cash deposit module."""

from __future__ import annotations

from collections import defaultdict
from datetime import date, datetime, timezone
from decimal import Decimal
from typing import Optional

from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.models.expenses import Expense
from app.models.operational import LaporanShift, PenjualanNozzle, StatusLaporan
from app.models.penyetoran import Penyetoran, PenyetoranBatch, StatusPenyetoran, StatusPenyetoranBatch
from app.models.spbu import Island, Nozzle, Shift
from app.repositories import penyetoran_repository
from app.schemas.penyetoran import (
    BatchReviewRequest,
    DailySummary,
    PenyetoranBatchCreate,
    PenyetoranBatchResponse,
    PenyetoranResponse,
    PenyetoranUpdate,
    ProductTotal,
    ShiftProductSummary,
    ShiftSummary,
)
from app.utils.file_upload import save_upload, UploadContext


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _build_response(p: Penyetoran) -> PenyetoranResponse:
    """Build a PenyetoranResponse from an ORM Penyetoran object."""
    # shift_nama requires laporan_shift.shift to be eagerly loaded
    shift_nama: Optional[str] = None
    if p.laporan_shift is not None and p.laporan_shift.shift is not None:
        shift_nama = p.laporan_shift.shift.nama

    return PenyetoranResponse(
        id=p.id,
        spbu_id=p.spbu_id,
        laporan_shift_id=p.laporan_shift_id,
        tanggal=p.tanggal,
        shift_id=p.shift_id,
        shift_nama=shift_nama,
        jumlah_kas=p.jumlah_kas,
        jumlah_non_kas=p.jumlah_non_kas,
        total_penjualan=p.total_penjualan,
        catatan=p.catatan,
        bukti_url=p.bukti_url,
        status=p.status if isinstance(p.status, str) else p.status.value,
        batch_id=p.batch_id,
        created_by_name=p.created_by.name if p.created_by else None,
        created_at=p.created_at,
        updated_at=p.updated_at,
    )


def _build_batch_response(batch: PenyetoranBatch) -> PenyetoranBatchResponse:
    """Build a PenyetoranBatchResponse from an ORM PenyetoranBatch object."""
    return PenyetoranBatchResponse(
        id=batch.id,
        spbu_id=batch.spbu_id,
        tanggal_from=batch.tanggal_from,
        tanggal_to=batch.tanggal_to,
        total_amount=batch.total_amount,
        catatan=batch.catatan,
        status=batch.status if isinstance(batch.status, str) else batch.status.value,
        submitted_by_name=batch.submitted_by.name if batch.submitted_by else None,
        submitted_at=batch.submitted_at,
        reviewed_by_name=batch.reviewed_by.name if batch.reviewed_by else None,
        reviewed_at=batch.reviewed_at,
        catatan_review=batch.catatan_review,
        items=[_build_response(p) for p in (batch.items or [])],
        created_at=batch.created_at,
        updated_at=batch.updated_at,
    )


async def _get_daily_sales_data(
    db: AsyncSession,
    spbu_id: int,
    tanggal: date,
) -> list[LaporanShift]:
    """
    Load all LaporanShift for the given SPBU + date, eagerly loading the
    nozzle rows and their related produk/island/shift for aggregation.
    """
    result = await db.execute(
        select(LaporanShift)
        .where(LaporanShift.spbu_id == spbu_id, LaporanShift.tanggal == tanggal)
        .options(
            selectinload(LaporanShift.shift),
            selectinload(LaporanShift.penjualan_nozzle).selectinload(
                PenjualanNozzle.nozzle
            ).selectinload(Nozzle.produk),
            selectinload(LaporanShift.penjualan_nozzle).selectinload(
                PenjualanNozzle.nozzle
            ).selectinload(Nozzle.island),
        )
        .order_by(LaporanShift.id)
    )
    return list(result.scalars().unique().all())


async def _get_daily_expenses_total(
    db: AsyncSession, spbu_id: int, tanggal: date
) -> Decimal:
    """Sum all Expense.jumlah for a given SPBU + date."""
    result = await db.execute(
        select(func.coalesce(func.sum(Expense.jumlah), 0))
        .where(Expense.spbu_id == spbu_id, Expense.tanggal == tanggal)
    )
    return Decimal(str(result.scalar_one()))


def _aggregate_shift(laporan: LaporanShift) -> ShiftSummary:
    """Build a ShiftSummary by aggregating PenjualanNozzle rows per product."""
    # Group nozzle rows by produk_id
    per_produk: dict[int, dict] = defaultdict(lambda: {
        "volume_digital": Decimal("0"),
        "volume_manual": Decimal("0"),
        "volume_final": Decimal("0"),
        "nilai": Decimal("0"),
        "harga_jual": Decimal("0"),
        "produk_nama": "",
        "produk_kode": "",
        "produk_id": 0,
    })

    for row in laporan.penjualan_nozzle or []:
        nozzle = row.nozzle
        produk = nozzle.produk if nozzle else None
        produk_id = produk.id if produk else 0

        entry = per_produk[produk_id]
        entry["produk_id"] = produk_id
        entry["produk_nama"] = produk.nama if produk else ""
        entry["produk_kode"] = produk.kode if produk else ""
        entry["harga_jual"] = row.harga_jual  # last-write wins — same produk same price

        # Digital volume: teller_akhir_digital - teller_awal_digital
        # If flag_reset_teller, the stored `volume` already accounts for that correctly
        # so we use the stored volume as the final figure and do a best-effort for
        # manual/digital breakdown.
        if row.flag_reset_teller:
            # We cannot safely subtract tellers when reset — use stored volume for final
            # and approximate digital/manual from the stored volume
            entry["volume_digital"] += row.volume
            entry["volume_manual"] += row.volume
        else:
            entry["volume_digital"] += row.teller_akhir_digital - row.teller_awal_digital
            entry["volume_manual"] += row.teller_akhir_manual - row.teller_awal_manual

        entry["volume_final"] += row.volume  # stored from primary_teller at save time
        entry["nilai"] += row.nilai

    products = [
        ShiftProductSummary(
            produk_id=v["produk_id"],
            produk_nama=v["produk_nama"],
            produk_kode=v["produk_kode"],
            volume_digital=v["volume_digital"],
            volume_manual=v["volume_manual"],
            volume_final=v["volume_final"],
            nilai=v["nilai"],
            harga_jual=v["harga_jual"],
        )
        for v in per_produk.values()
    ]

    total_vd = sum(p.volume_digital for p in products)
    total_vm = sum(p.volume_manual for p in products)
    total_vf = sum(p.volume_final for p in products)
    total_n = sum(p.nilai for p in products)

    return ShiftSummary(
        laporan_shift_id=laporan.id,
        shift_id=laporan.shift_id,
        shift_nama=laporan.shift.nama if laporan.shift else "",
        status=laporan.status if isinstance(laporan.status, str) else laporan.status.value,
        products=products,
        total_volume_digital=total_vd,
        total_volume_manual=total_vm,
        total_volume_final=total_vf,
        total_nilai=total_n,
    )


# ---------------------------------------------------------------------------
# Public service functions — daily summary
# ---------------------------------------------------------------------------

async def get_daily_summary(
    db: AsyncSession, spbu_id: int, tanggal: date
) -> DailySummary:
    """
    Aggregate all laporan_shift + penjualan_nozzle data for a date into a DailySummary.
    Also checks for an existing penyetoran record and includes its id/status.
    """
    laporan_list = await _get_daily_sales_data(db, spbu_id, tanggal)
    total_expenses = await _get_daily_expenses_total(db, spbu_id, tanggal)

    shifts: list[ShiftSummary] = [_aggregate_shift(ls) for ls in laporan_list]

    # Cross-shift product totals
    cross_produk: dict[int, dict] = defaultdict(lambda: {
        "volume_digital": Decimal("0"),
        "volume_manual": Decimal("0"),
        "volume_final": Decimal("0"),
        "nilai": Decimal("0"),
        "harga_jual": Decimal("0"),
        "produk_nama": "",
        "produk_kode": "",
        "produk_id": 0,
    })
    for shift in shifts:
        for p in shift.products:
            e = cross_produk[p.produk_id]
            e["produk_id"] = p.produk_id
            e["produk_nama"] = p.produk_nama
            e["produk_kode"] = p.produk_kode
            e["harga_jual"] = p.harga_jual
            e["volume_digital"] += p.volume_digital
            e["volume_manual"] += p.volume_manual
            e["volume_final"] += p.volume_final
            e["nilai"] += p.nilai

    products_total = [
        ProductTotal(
            produk_id=v["produk_id"],
            produk_nama=v["produk_nama"],
            produk_kode=v["produk_kode"],
            volume_digital=v["volume_digital"],
            volume_manual=v["volume_manual"],
            volume_final=v["volume_final"],
            nilai=v["nilai"],
            harga_jual=v["harga_jual"],
        )
        for v in cross_produk.values()
    ]

    grand_vd = sum(p.volume_digital for p in products_total)
    grand_vm = sum(p.volume_manual for p in products_total)
    grand_vf = sum(p.volume_final for p in products_total)
    grand_n = sum(p.nilai for p in products_total)
    suggested = grand_n - total_expenses

    # Check for an already existing penyetoran on this date (returns first match)
    existing = await penyetoran_repository.get_by_tanggal(db, spbu_id, tanggal)

    return DailySummary(
        tanggal=tanggal,
        shifts=shifts,
        products_total=products_total,
        grand_total_volume_digital=grand_vd,
        grand_total_volume_manual=grand_vm,
        grand_total_volume_final=grand_vf,
        grand_total_nilai=grand_n,
        total_expenses=total_expenses,
        suggested_setor=suggested,
        existing_penyetoran_id=existing.id if existing else None,
        existing_penyetoran_status=existing.status if existing else None,
    )


# ---------------------------------------------------------------------------
# Public service functions — penyetoran CRUD
# ---------------------------------------------------------------------------

async def list_penyetoran(
    db: AsyncSession,
    spbu_id: int,
    skip: int = 0,
    limit: int = 50,
    tanggal_from: Optional[date] = None,
    tanggal_to: Optional[date] = None,
    status: Optional[str] = None,
) -> tuple[list[PenyetoranResponse], int]:
    """Return a paginated list of PenyetoranResponse for an SPBU."""
    rows, total = await penyetoran_repository.get_all(
        db, spbu_id, skip, limit,
        tanggal_from=tanggal_from,
        tanggal_to=tanggal_to,
        status=status,
    )
    return [_build_response(p) for p in rows], total


async def get_penyetoran_detail(
    db: AsyncSession, spbu_id: int, penyetoran_id: int
) -> PenyetoranResponse:
    """Return a single PenyetoranResponse, raising ValueError if not found."""
    p = await penyetoran_repository.get_by_id(db, penyetoran_id, spbu_id)
    if p is None:
        raise ValueError("Penyetoran tidak ditemukan")
    return _build_response(p)


async def update_penyetoran(
    db: AsyncSession,
    spbu_id: int,
    penyetoran_id: int,
    body: PenyetoranUpdate,
) -> PenyetoranResponse:
    """Update catatan on a DRAFT penyetoran."""
    p = await penyetoran_repository.get_by_id(db, penyetoran_id, spbu_id)
    if p is None:
        raise ValueError("Penyetoran tidak ditemukan")
    if p.status != StatusPenyetoran.DRAFT:
        raise ValueError("Hanya penyetoran berstatus DRAFT yang dapat diedit")

    update_dict: dict = {}
    if body.catatan is not None:
        update_dict["catatan"] = body.catatan

    if not update_dict:
        return _build_response(p)

    try:
        p = await penyetoran_repository.update(db, p, update_dict)
        await db.commit()
        return _build_response(p)
    except IntegrityError:
        await db.rollback()
        raise ValueError("Constraint error pada penyetoran")
    except SQLAlchemyError:
        await db.rollback()
        raise


async def upload_bukti(
    db: AsyncSession,
    spbu_id: int,
    penyetoran_id: int,
    file_bytes: bytes,
    filename: str,
) -> PenyetoranResponse:
    """Save proof-of-transfer image/PDF and store the URL on the penyetoran record."""
    p = await penyetoran_repository.get_by_id(db, penyetoran_id, spbu_id)
    if p is None:
        raise ValueError("Penyetoran tidak ditemukan")
    if p.status != StatusPenyetoran.DRAFT:
        raise ValueError("Bukti hanya bisa diupload pada penyetoran DRAFT")

    max_bytes = 10 * 1024 * 1024
    if len(file_bytes) > max_bytes:
        raise ValueError("Ukuran file melebihi batas maksimum 10 MB")

    ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
    if ext not in {"pdf", "jpg", "jpeg", "png"}:
        raise ValueError("Tipe file tidak didukung. Gunakan PDF, JPG, atau PNG.")

    from app.utils.file_upload import get_spbu_code
    spbu_code = await get_spbu_code(db, p.spbu_id)
    ctx = UploadContext(spbu_code, "penyetoran", p.tanggal)
    url = await save_upload(file_bytes, filename, ctx)
    try:
        p = await penyetoran_repository.update_bukti(db, p, url)
        await db.commit()
        return _build_response(p)
    except IntegrityError:
        await db.rollback()
        raise ValueError("Constraint error pada penyetoran")
    except SQLAlchemyError:
        await db.rollback()
        raise


# ---------------------------------------------------------------------------
# Public service functions — batch
# ---------------------------------------------------------------------------

async def list_batches(
    db: AsyncSession,
    spbu_id: int,
    skip: int = 0,
    limit: int = 50,
    status: Optional[str] = None,
) -> tuple[list[PenyetoranBatchResponse], int]:
    """Return paginated PenyetoranBatch list for an SPBU."""
    rows, total = await penyetoran_repository.get_all_batches(db, spbu_id, skip, limit, status=status)
    return [_build_batch_response(b) for b in rows], total


async def get_batch_detail(
    db: AsyncSession, spbu_id: int, batch_id: int
) -> PenyetoranBatchResponse:
    """Return a single PenyetoranBatchResponse, raising ValueError if not found."""
    batch = await penyetoran_repository.get_batch_by_id(db, batch_id, spbu_id)
    if batch is None:
        raise ValueError("Batch tidak ditemukan")
    return _build_batch_response(batch)


async def create_batch(
    db: AsyncSession,
    spbu_id: int,
    user_id: int,
    body: PenyetoranBatchCreate,
) -> PenyetoranBatchResponse:
    """
    Create a PenyetoranBatch from a list of DRAFT penyetoran IDs.

    Validates all penyetoran belong to spbu_id and are DRAFT.
    Sets their status to SUBMITTED and links them to the new batch.
    Does NOT set submitted_by_id on the batch — that's set by submit_batch().
    """
    if not body.penyetoran_ids:
        raise ValueError("Pilih minimal satu penyetoran")

    # Load and validate all penyetoran
    penyetoran_list: list[Penyetoran] = []
    for pid in body.penyetoran_ids:
        p = await penyetoran_repository.get_by_id(db, pid, spbu_id)
        if p is None:
            raise ValueError(f"Penyetoran id={pid} tidak ditemukan di SPBU ini")
        if p.status != StatusPenyetoran.DRAFT:
            raise ValueError(
                f"Penyetoran id={pid} berstatus {p.status}, hanya DRAFT yang bisa di-batch"
            )
        penyetoran_list.append(p)

    total_amount = sum(p.total_penjualan for p in penyetoran_list)

    batch_data = {
        "tanggal_from": body.tanggal_from,
        "tanggal_to": body.tanggal_to,
        "total_amount": total_amount,
        "catatan": body.catatan,
        "status": StatusPenyetoranBatch.DRAFT,
    }

    try:
        batch = await penyetoran_repository.create_batch(db, spbu_id, batch_data)

        # Link each penyetoran to the batch and set status to SUBMITTED
        for p in penyetoran_list:
            await penyetoran_repository.update(db, p, {
                "batch_id": batch.id,
                "status": StatusPenyetoran.SUBMITTED,
            })

        await db.commit()
        # Re-fetch to get fresh items list
        batch = await penyetoran_repository.get_batch_by_id(db, batch.id, spbu_id)
        return _build_batch_response(batch)  # type: ignore[arg-type]
    except IntegrityError:
        await db.rollback()
        raise ValueError("Constraint error saat membuat batch")
    except SQLAlchemyError:
        await db.rollback()
        raise


async def submit_batch(
    db: AsyncSession,
    spbu_id: int,
    batch_id: int,
    user_id: int,
) -> PenyetoranBatchResponse:
    """
    Submit a DRAFT batch to Manager for approval.
    Sets status=SUBMITTED, submitted_by_id, submitted_at.
    """
    batch = await penyetoran_repository.get_batch_by_id(db, batch_id, spbu_id)
    if batch is None:
        raise ValueError("Batch tidak ditemukan")
    if batch.status != StatusPenyetoranBatch.DRAFT:
        raise ValueError("Hanya batch berstatus DRAFT yang dapat di-submit")

    try:
        batch = await penyetoran_repository.update_batch(db, batch, {
            "status": StatusPenyetoranBatch.SUBMITTED,
            "submitted_by_id": user_id,
            "submitted_at": datetime.now(timezone.utc),
        })
        await db.commit()
        return _build_batch_response(batch)
    except IntegrityError:
        await db.rollback()
        raise ValueError("Constraint error saat submit batch")
    except SQLAlchemyError:
        await db.rollback()
        raise


async def review_batch(
    db: AsyncSession,
    spbu_id: int,
    batch_id: int,
    user_id: int,
    body: BatchReviewRequest,
) -> PenyetoranBatchResponse:
    """
    Manager approves a SUBMITTED batch.
    On approve: batch→APPROVED, all linked penyetoran→APPROVED.
    Only action='approve' is supported (PenyetoranBatch has no REJECTED status).
    """
    batch = await penyetoran_repository.get_batch_by_id(db, batch_id, spbu_id)
    if batch is None:
        raise ValueError("Batch tidak ditemukan")
    if batch.status != StatusPenyetoranBatch.SUBMITTED:
        raise ValueError("Review hanya bisa dilakukan pada batch berstatus SUBMITTED")
    if body.action != "approve":
        raise ValueError("Action harus 'approve'")

    try:
        now = datetime.now(timezone.utc)
        batch = await penyetoran_repository.update_batch(db, batch, {
            "status": StatusPenyetoranBatch.APPROVED,
            "reviewed_by_id": user_id,
            "reviewed_at": now,
            "catatan_review": body.catatan_review,
        })

        # Approve all linked penyetoran
        for p in batch.items or []:
            await penyetoran_repository.update(db, p, {
                "status": StatusPenyetoran.APPROVED,
            })

        await db.commit()
        # Re-fetch to get fresh items list after updates
        batch = await penyetoran_repository.get_batch_by_id(db, batch_id, spbu_id)
        return _build_batch_response(batch)  # type: ignore[arg-type]
    except IntegrityError:
        await db.rollback()
        raise ValueError("Constraint error saat review batch")
    except SQLAlchemyError:
        await db.rollback()
        raise
