"""Stock adjustment service — business logic for sounding / stock adjustment module."""

from datetime import date
from decimal import Decimal

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

from app.models.operational import StockAdjustment, StatusLaporan
from app.models.role import AksiEnum, ModulEnum
from app.models.spbu import Tangki
from app.repositories import stock_repository, role_repository
from app.schemas.stock import (
    StockAdjustmentItemInput,
    StockAdjustmentItemResponse,
    StockAdjustmentResponse,
    StockAdjustmentDetailResponse,
    StockInitResponse,
    StockItemFotoResponse,
)
from app.utils.kalibrasi import interpolate_volume


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

async def _interpolate_for_tank(db: AsyncSession, tangki: Tangki, height_mm: Decimal) -> Decimal:
    """
    Load kalibrasi rows for a tank and interpolate height_mm → volume.
    Raises ValueError with tank name included if out of range or table empty.
    """
    # kalibrasi is eagerly loaded when tangki is loaded with selectinload(Tangki.kalibrasi)
    kalibrasi_rows = tangki.kalibrasi
    try:
        return interpolate_volume(height_mm, kalibrasi_rows)
    except ValueError as e:
        raise ValueError(f"Tangki '{tangki.nama}': {e}") from e


async def _process_items(
    db: AsyncSession,
    item_inputs: list[StockAdjustmentItemInput],
    spbu_id: int,
) -> list[dict]:
    """
    Validate each item, load tangki, interpolate dipstick readings, compute volume_final.

    Rules:
    - tangki must belong to spbu_id
    - digital dipstick is always required and always interpolated
    - manual dipstick is optional (None = not measured); if provided, also interpolated
    - volume_final = manual if available, else digital
    """
    rows: list[dict] = []
    for inp in item_inputs:
        # Load tangki with kalibrasi for interpolation
        result = await db.execute(
            select(Tangki)
            .where(Tangki.id == inp.tangki_id, Tangki.deleted_at.is_(None))
            .options(
                selectinload(Tangki.kalibrasi),
                selectinload(Tangki.produk),
            )
        )
        tangki = result.scalar_one_or_none()
        if tangki is None:
            raise ValueError(f"Tangki id={inp.tangki_id} tidak ditemukan")
        if tangki.spbu_id != spbu_id:
            raise ValueError(f"Tangki id={inp.tangki_id} tidak termasuk SPBU ini")

        # Interpolate digital (always required), but respect user override
        if inp.volume_digital_liter_override is not None:
            volume_digital = inp.volume_digital_liter_override
        else:
            volume_digital = await _interpolate_for_tank(db, tangki, inp.dipstick_digital_mm)

        # Interpolate manual (optional)
        volume_manual = None
        if inp.dipstick_manual_mm is not None:
            volume_manual = await _interpolate_for_tank(db, tangki, inp.dipstick_manual_mm)

        # volume_final: manual if available, else digital
        volume_final = volume_manual if volume_manual is not None else volume_digital

        rows.append({
            "tangki_id": inp.tangki_id,
            "dipstick_manual_mm": inp.dipstick_manual_mm,
            "volume_manual_liter": volume_manual,
            "dipstick_digital_mm": inp.dipstick_digital_mm,
            "volume_digital_liter": volume_digital,
            "volume_final_liter": volume_final,
        })
    return rows


def _build_response(adj: StockAdjustment) -> StockAdjustmentResponse:
    """Build a summary StockAdjustmentResponse from an ORM object."""
    items = adj.items or []
    total_volume = sum(
        (item.volume_final_liter for item in items if item.volume_final_liter is not None),
        Decimal("0"),
    )
    return StockAdjustmentResponse(
        id=adj.id,
        spbu_id=adj.spbu_id,
        shift_id=adj.shift_id,
        shift_nama=adj.shift.nama if adj.shift else "",
        tanggal=adj.tanggal,
        status=adj.status,
        submitted_by_name=adj.submitted_by.name if adj.submitted_by else None,
        submitted_at=adj.submitted_at,
        reviewed_by_name=adj.reviewed_by.name if adj.reviewed_by else None,
        reviewed_at=adj.reviewed_at,
        catatan_review=adj.catatan_review,
        unlock_reason=adj.unlock_reason,
        recalled_by_name=adj.recalled_by.name if adj.recalled_by else None,
        recalled_at=adj.recalled_at,
        unlocked_by_name=adj.unlocked_by.name if adj.unlocked_by else None,
        unlocked_at=adj.unlocked_at,
        item_count=len(items),
        total_volume_final=total_volume,
    )


def _build_item_response(item) -> StockAdjustmentItemResponse:
    """Build an enriched StockAdjustmentItemResponse from a StockAdjustmentItem ORM row."""
    tangki = item.tangki
    produk = tangki.produk if tangki else None
    fotos = [
        StockItemFotoResponse(
            id=f.id,
            stock_adjustment_item_id=f.stock_adjustment_item_id,
            tipe=f.tipe,
            url=f.url,
        )
        for f in (item.fotos or [])
    ]
    return StockAdjustmentItemResponse(
        id=item.id,
        tangki_id=item.tangki_id,
        tangki_nama=tangki.nama if tangki else "",
        produk_nama=produk.nama if produk else "",
        kapasitas_liter=tangki.kapasitas_liter if tangki else Decimal("0"),
        dipstick_manual_mm=item.dipstick_manual_mm,
        volume_manual_liter=item.volume_manual_liter,
        dipstick_digital_mm=item.dipstick_digital_mm,
        volume_digital_liter=item.volume_digital_liter,
        volume_final_liter=item.volume_final_liter,
        fotos=fotos,
    )


def _build_detail_response(adj: StockAdjustment) -> StockAdjustmentDetailResponse:
    """Build a full StockAdjustmentDetailResponse including all item rows."""
    summary = _build_response(adj)
    item_responses = [_build_item_response(item) for item in (adj.items or [])]
    return StockAdjustmentDetailResponse(
        **summary.model_dump(),
        items=item_responses,
    )


async def _user_can_approve(db: AsyncSession, user, spbu_id: int) -> bool:
    """Return True if user has stock: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.stock, AksiEnum.approve
    )


# ---------------------------------------------------------------------------
# Public service functions
# ---------------------------------------------------------------------------

async def get_stock_init(
    db: AsyncSession, spbu_id: int
) -> list[StockInitResponse]:
    """Return pre-fill data (last known volume/dipstick) for all active tanks in an SPBU."""
    rows = await stock_repository.get_stock_init(db, spbu_id)
    return [
        StockInitResponse(
            tangki_id=r["tangki_id"],
            tangki_nama=r["tangki_nama"],
            produk_nama=r["produk_nama"],
            kapasitas_liter=r["kapasitas_liter"],
            last_volume_final=r["last_volume_final"],
            last_dipstick_digital_mm=r["last_dipstick_digital_mm"],
            is_first_time=r["is_first_time"],
        )
        for r in rows
    ]


async def list_adjustments(
    db: AsyncSession,
    spbu_id: int,
    tanggal=None,
    shift_id: int | None = None,
    status: StatusLaporan | None = None,
    skip: int = 0,
    limit: int = 20,
) -> tuple[list[StockAdjustmentResponse], int]:
    """Return a paginated list of StockAdjustmentResponse for an SPBU."""
    adj_list, total = await stock_repository.get_all(db, spbu_id, tanggal, shift_id, status, skip, limit)
    return [_build_response(a) for a in adj_list], total


async def get_detail(
    db: AsyncSession, spbu_id: int, adj_id: int
) -> StockAdjustmentDetailResponse:
    """Return a fully enriched StockAdjustmentDetailResponse."""
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    if adj is None:
        raise ValueError("Stock adjustment tidak ditemukan")
    return _build_detail_response(adj)


async def create_adjustment(
    db: AsyncSession,
    spbu_id: int,
    shift_id: int,
    tanggal: date,
    item_inputs: list[StockAdjustmentItemInput],
    submitter_user: object,
    force_draft: bool = False,
) -> StockAdjustmentDetailResponse:
    """
    Create a new StockAdjustment.

    - Operators → saved as DRAFT
    - Admins (stock:approve permission) → immediately APPROVED in one step

    Raises ValueError if a record for the same spbu+shift+tanggal already exists,
    or on any tank/interpolation validation failure.
    """
    dup = await db.execute(
        select(StockAdjustment).where(
            and_(
                StockAdjustment.spbu_id == spbu_id,
                StockAdjustment.shift_id == shift_id,
                StockAdjustment.tanggal == tanggal,
            )
        )
    )
    if dup.scalar_one_or_none() is not None:
        raise ValueError(
            f"Stock adjustment untuk shift_id={shift_id} pada tanggal {tanggal} sudah ada"
        )

    item_rows = await _process_items(db, item_inputs, spbu_id)

    try:
        adj = await stock_repository.create(db, spbu_id, shift_id, tanggal, item_rows)

        can_approve = (await _user_can_approve(db, submitter_user, spbu_id)) and not force_draft
        if can_approve:
            from datetime import datetime, timezone
            # Set submitted fields before the status update so they land in the same commit
            adj.submitted_by_id = submitter_user.id  # type: ignore[union-attr]
            adj.submitted_at = datetime.now(timezone.utc)
            adj = await stock_repository.update_status(
                db, adj, StatusLaporan.APPROVED,
                user_id=submitter_user.id,  # type: ignore[union-attr]
                catatan="Auto-approved oleh admin saat input",
            )

        await db.commit()
    except IntegrityError:
        await db.rollback()
        raise ValueError(
            f"Stock adjustment untuk shift_id={shift_id} pada tanggal {tanggal} sudah ada"
        )
    except SQLAlchemyError:
        await db.rollback()
        raise

    return _build_detail_response(adj)


async def update_adjustment(
    db: AsyncSession,
    spbu_id: int,
    adj_id: int,
    item_inputs: list[StockAdjustmentItemInput],
) -> StockAdjustmentDetailResponse:
    """Replace tank rows on a DRAFT stock adjustment."""
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    if adj is None:
        raise ValueError("Stock adjustment tidak ditemukan")
    if adj.spbu_id != spbu_id:
        raise ValueError("Stock adjustment tidak termasuk SPBU ini")
    if adj.status != StatusLaporan.DRAFT:
        raise ValueError("Hanya stock adjustment berstatus DRAFT yang dapat diedit")

    item_rows = await _process_items(db, item_inputs, spbu_id)

    try:
        adj = await stock_repository.update_items(db, adj, item_rows)
        await db.commit()
    except IntegrityError:
        await db.rollback()
        raise ValueError("Gagal menyimpan perubahan item stock adjustment")
    except SQLAlchemyError:
        await db.rollback()
        raise

    return _build_detail_response(adj)


async def submit_adjustment(
    db: AsyncSession, spbu_id: int, adj_id: int, user_id: int
) -> StockAdjustmentDetailResponse:
    """
    Transition a DRAFT adjustment to SUBMITTED.
    Validates that all active tanks in the SPBU have items AND all digital readings are filled.
    """
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    if adj is None:
        raise ValueError("Stock adjustment tidak ditemukan")
    if adj.status != StatusLaporan.DRAFT:
        raise ValueError("Hanya stock adjustment berstatus DRAFT yang dapat di-submit")

    # Validate all active tanks are present
    tanks_result = await db.execute(
        select(Tangki)
        .where(
            Tangki.spbu_id == spbu_id,
            Tangki.is_active.is_(True),
            Tangki.deleted_at.is_(None),
        )
    )
    active_tanks = list(tanks_result.scalars().all())
    active_tank_ids = {t.id for t in active_tanks}
    item_tank_ids = {item.tangki_id for item in (adj.items or [])}
    missing = active_tank_ids - item_tank_ids
    if missing:
        missing_names = [t.nama for t in active_tanks if t.id in missing]
        raise ValueError(
            f"Semua tangki aktif harus memiliki data sounding sebelum submit. "
            f"Tangki yang belum diisi: {', '.join(missing_names)}"
        )

    # Validate all digital readings are present
    for item in (adj.items or []):
        if item.dipstick_digital_mm is None or item.volume_digital_liter is None:
            raise ValueError(
                f"Tangki id={item.tangki_id}: pembacaan digital wajib diisi sebelum submit"
            )

    try:
        adj = await stock_repository.update_status(
            db, adj, StatusLaporan.SUBMITTED, user_id=user_id
        )
        await db.commit()
    except IntegrityError:
        await db.rollback()
        raise ValueError("Gagal submit stock adjustment")
    except SQLAlchemyError:
        await db.rollback()
        raise

    return _build_detail_response(adj)


async def recall_adjustment(
    db: AsyncSession, spbu_id: int, adj_id: int, user_id: int
) -> StockAdjustmentDetailResponse:
    """Pull a SUBMITTED adjustment back to DRAFT (operator recall)."""
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    if adj is None:
        raise ValueError("Stock adjustment tidak ditemukan")
    if adj.status != StatusLaporan.SUBMITTED:
        raise ValueError("Recall hanya bisa dilakukan pada stock adjustment berstatus SUBMITTED")

    try:
        adj = await stock_repository.update_status(
            db, adj, StatusLaporan.DRAFT, user_id=user_id, recall=True
        )
        await db.commit()
    except IntegrityError:
        await db.rollback()
        raise ValueError("Gagal recall stock adjustment")
    except SQLAlchemyError:
        await db.rollback()
        raise

    return _build_detail_response(adj)


async def review_adjustment(
    db: AsyncSession,
    spbu_id: int,
    adj_id: int,
    user_id: int,
    action: str,
    catatan: str | None,
) -> StockAdjustmentDetailResponse:
    """Approve or reject a SUBMITTED adjustment."""
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    if adj is None:
        raise ValueError("Stock adjustment tidak ditemukan")
    if adj.status != StatusLaporan.SUBMITTED:
        raise ValueError("Hanya stock adjustment berstatus SUBMITTED yang dapat di-review")

    new_status = StatusLaporan.APPROVED if action == "approve" else StatusLaporan.REJECTED

    try:
        adj = await stock_repository.update_status(
            db, adj, new_status, user_id=user_id, catatan=catatan
        )
        await db.commit()
    except IntegrityError:
        await db.rollback()
        raise ValueError("Gagal menyimpan hasil review stock adjustment")
    except SQLAlchemyError:
        await db.rollback()
        raise

    return _build_detail_response(adj)


async def unlock_adjustment(
    db: AsyncSession, spbu_id: int, adj_id: int, user_id: int, alasan: str
) -> StockAdjustmentDetailResponse:
    """Reopen an APPROVED or LOCKED adjustment back to DRAFT, recording the reason."""
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    if adj is None:
        raise ValueError("Stock adjustment tidak ditemukan")
    if adj.status not in (StatusLaporan.APPROVED, StatusLaporan.LOCKED):
        raise ValueError("Hanya stock adjustment berstatus APPROVED atau LOCKED yang dapat di-unlock")

    try:
        from datetime import datetime, timezone
        adj = await stock_repository.update_status(
            db, adj, StatusLaporan.DRAFT, unlock_reason=alasan
        )
        adj.unlocked_by_id = user_id
        adj.unlocked_at = datetime.now(timezone.utc)
        await db.commit()
        await db.refresh(adj)
    except IntegrityError:
        await db.rollback()
        raise ValueError("Gagal unlock stock adjustment")
    except SQLAlchemyError:
        await db.rollback()
        raise

    return _build_detail_response(adj)


async def calculate_volume_for_tank(
    db: AsyncSession, spbu_id: int, tangki_id: int, height_mm: Decimal
) -> Decimal:
    """Lookup volume for a given dipstick height from a tank's calibration table."""
    result = await db.execute(
        select(Tangki)
        .where(Tangki.id == tangki_id, Tangki.deleted_at.is_(None))
        .options(selectinload(Tangki.kalibrasi))
    )
    tangki = result.scalar_one_or_none()
    if tangki is None:
        raise ValueError(f"Tangki id={tangki_id} tidak ditemukan")
    if tangki.spbu_id != spbu_id:
        raise ValueError(f"Tangki id={tangki_id} tidak termasuk SPBU ini")
    return await _interpolate_for_tank(db, tangki, height_mm)


async def check_user_can_approve(db: AsyncSession, user, spbu_id: int) -> bool:
    """Public wrapper — returns True if user can approve stock adjustments for this SPBU."""
    return await _user_can_approve(db, user, spbu_id)


async def upload_item_foto(
    db: AsyncSession,
    spbu_id: int,
    adj_id: int,
    item_id: int,
    tipe: str,
    file_bytes: bytes,
    filename: str,
) -> StockAdjustmentDetailResponse:
    """
    Save a photo for a StockAdjustmentItem (tipe: 'manual' | 'digital').
    Returns the refreshed full detail response.
    """
    from app.utils.file_upload import save_upload, UploadContext

    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    if adj is None:
        raise ValueError("Stock adjustment tidak ditemukan")

    if adj.status != StatusLaporan.DRAFT:
        raise ValueError("Foto hanya bisa diubah pada stock adjustment berstatus DRAFT")

    item = await stock_repository.get_item_by_id(db, item_id, adj_id)
    if item is None:
        raise ValueError("Item stock adjustment tidak ditemukan")

    if tipe not in ("manual", "digital"):
        raise ValueError("Tipe foto harus 'manual' atau 'digital'")

    from app.utils.file_upload import get_spbu_code
    spbu_code = await get_spbu_code(db, spbu_id)
    ctx = UploadContext(spbu_code, "stock", adj.tanggal)
    url = await save_upload(file_bytes, filename, ctx)

    try:
        await stock_repository.add_item_foto(db, item_id, tipe, url)
        await db.commit()
    except IntegrityError:
        await db.rollback()
        raise ValueError("Gagal menyimpan foto item stock adjustment")
    except SQLAlchemyError:
        await db.rollback()
        raise

    # Reload full detail
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    return _build_detail_response(adj)


async def delete_item_foto(
    db: AsyncSession,
    spbu_id: int,
    adj_id: int,
    item_id: int,
    foto_id: int,
) -> StockAdjustmentDetailResponse:
    """
    Delete a photo from a StockAdjustmentItem.
    Returns the refreshed full detail response.
    """
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    if adj is None:
        raise ValueError("Stock adjustment tidak ditemukan")

    if adj.status != StatusLaporan.DRAFT:
        raise ValueError("Foto hanya bisa dihapus pada stock adjustment berstatus DRAFT")

    item = await stock_repository.get_item_by_id(db, item_id, adj_id)
    if item is None:
        raise ValueError("Item stock adjustment tidak ditemukan")

    from app.utils.file_upload import delete_file
    try:
        deleted = await stock_repository.delete_item_foto(db, foto_id, item_id)
        if deleted is None:
            raise ValueError("Foto tidak ditemukan")
        await db.commit()
    except ValueError:
        await db.rollback()
        raise
    except IntegrityError:
        await db.rollback()
        raise ValueError("Gagal menghapus foto item stock adjustment")
    except SQLAlchemyError:
        await db.rollback()
        raise
    await delete_file(deleted.url)
    adj = await stock_repository.get_by_id(db, adj_id, spbu_id)
    return _build_detail_response(adj)
