from datetime import date
from decimal import Decimal

from app.sales.operational_repository import SalesOperationalRepository
from app.sales.schemas import (
    SalesExpeditionRequest,
    SalesFifoShortageResponse,
    SalesMissingNumberResponse,
    SalesNumberPreviewResponse,
    SalesOperationalUpdateResponse,
    SalesPackageRequest,
    SalesReusableNumberResponse,
    SalesHistoryResponse,
    SalesStatusReconcileRequest,
    SalesStatusReconcileResponse,
    SalesPrintPayload,
    SalesWhatsAppPayload,
)


class InvalidPeriodError(Exception):
    pass


class OperationalWriteDisabledError(Exception):
    pass


class OperationalNonTransactionalTablesError(Exception):
    def __init__(self, tables: list[str]) -> None:
        self.tables = tables


class OperationalSaleNotFoundError(Exception):
    pass


class SalesOperationalService:
    def __init__(
        self,
        repository: SalesOperationalRepository,
        *,
        write_enabled: bool = False,
    ) -> None:
        self._repository = repository
        self._write_enabled = write_enabled

    async def _ensure_writes_are_safe(self) -> None:
        if not self._write_enabled:
            raise OperationalWriteDisabledError()
        tables = await self._repository.non_transactional_tables()
        if tables:
            raise OperationalNonTransactionalTablesError(tables)
        await self._repository.session.rollback()

    @staticmethod
    def _period_prefix(periode: str) -> str:
        if len(periode) != 6 or not periode.isdigit():
            raise InvalidPeriodError("periode harus berformat YYYYMM")
        year = int(periode[:4])
        month = int(periode[4:])
        if year < 2000 or year > 2100 or month < 1 or month > 12:
            raise InvalidPeriodError("periode harus berformat YYYYMM")
        return f"{year % 100:02d}{month:02d}"

    async def preview_number(
        self, *, periode: str, kode_divisi: str
    ) -> SalesNumberPreviewResponse:
        period_prefix = self._period_prefix(periode)
        if len(kode_divisi) < 2 or not kode_divisi[-2:].isdigit():
            raise InvalidPeriodError("dua karakter terakhir kode_divisi harus angka")
        division = kode_divisi[-2:]
        prefix = f"SI{period_prefix}{division}"
        sequence = await self._repository.latest_sequence(prefix) + 1
        if sequence > 999999:
            raise InvalidPeriodError("Nomor SI untuk periode/divisi ini sudah habis")
        return SalesNumberPreviewResponse(
            periode=periode,
            kode_divisi=kode_divisi,
            prefix=prefix,
            nomor_preview=f"{prefix}{sequence:06d}",
            sequence=sequence,
        )

    async def reusable_numbers(self, periode: str) -> SalesReusableNumberResponse:
        prefix = self._period_prefix(periode)
        rows = await self._repository.reusable_numbers(prefix)
        return SalesReusableNumberResponse(periode=periode, data=rows)

    async def missing_numbers(
        self, *, periode: str, kode_divisi: str | None
    ) -> SalesMissingNumberResponse:
        prefix = self._period_prefix(periode)
        division = None
        if kode_divisi is not None:
            if len(kode_divisi) < 2 or not kode_divisi[-2:].isdigit():
                raise InvalidPeriodError(
                    "dua karakter terakhir kode_divisi harus angka"
                )
            division = kode_divisi[-2:]
        used = await self._repository.used_numbers(
            period_prefix=prefix, division=division
        )
        by_prefix: dict[str, set[int]] = {}
        for number in used:
            by_prefix.setdefault(number[:8], set()).add(int(number[-6:]))
        missing: list[str] = []
        truncated = False
        for number_prefix, sequences in sorted(by_prefix.items()):
            if not sequences:
                continue
            for sequence in range(1, max(sequences) + 1):
                if sequence not in sequences:
                    missing.append(f"{number_prefix}{sequence:06d}")
                    if len(missing) == 500:
                        truncated = True
                        break
            if truncated:
                break
        return SalesMissingNumberResponse(
            periode=periode,
            kode_divisi=kode_divisi,
            data=missing,
            truncated=truncated,
        )

    async def fifo_shortages(
        self, *, tanggal_mulai: date, tanggal_selesai: date
    ) -> SalesFifoShortageResponse:
        if tanggal_mulai > tanggal_selesai:
            raise ValueError("tanggal_mulai tidak boleh setelah tanggal_selesai")
        if (tanggal_selesai - tanggal_mulai).days > 366:
            raise ValueError("periode laporan maksimal 366 hari")
        rows = await self._repository.fifo_shortages(
            tanggal_mulai=tanggal_mulai,
            tanggal_selesai=tanggal_selesai,
        )
        return SalesFifoShortageResponse(
            tanggal_mulai=tanggal_mulai,
            tanggal_selesai=tanggal_selesai,
            data=rows,
        )

    async def reconcile_statuses(
        self,
        *,
        request: SalesStatusReconcileRequest,
        user_code: str,
        user_name: str,
    ) -> SalesStatusReconcileResponse:
        if request.preview_only:
            rows = await self._repository.reconcile_candidates(
                tanggal_mulai=request.tanggal_mulai,
                tanggal_selesai=request.tanggal_selesai,
            )
            return SalesStatusReconcileResponse(
                preview_only=True,
                mismatch_count=len(rows),
                updated_count=0,
                data=rows,
            )
        await self._ensure_writes_are_safe()
        async with self._repository.session.begin():
            rows = await self._repository.reconcile_candidates(
                tanggal_mulai=request.tanggal_mulai,
                tanggal_selesai=request.tanggal_selesai,
            )
            updated = await self._repository.apply_reconcile(
                rows=rows, user_code=user_code, user_name=user_name
            )
        return SalesStatusReconcileResponse(
            preview_only=False,
            mismatch_count=len(rows),
            updated_count=updated,
            data=rows,
        )

    async def update_expedition(
        self,
        *,
        kodesi: str,
        request: SalesExpeditionRequest,
        user_code: str,
        user_name: str,
    ) -> SalesOperationalUpdateResponse:
        await self._ensure_writes_are_safe()
        async with self._repository.session.begin():
            if not await self._repository.update_expedition(
                kodesi=kodesi,
                ekspedisi=request.ekspedisi,
                tanggal_ekspedisi=request.tanggal_ekspedisi,
            ):
                raise OperationalSaleNotFoundError(kodesi)
            await self._repository.add_audit(
                kodesi=kodesi,
                user_code=user_code,
                user_name=user_name,
                audit_status="UPDATE EKSPEDISI PENJUALAN",
            )
        return SalesOperationalUpdateResponse(
            kodesi=kodesi, status="expedition_updated"
        )

    async def update_packages(
        self,
        *,
        kodesi: str,
        request: SalesPackageRequest,
        user_code: str,
        user_name: str,
    ) -> SalesOperationalUpdateResponse:
        await self._ensure_writes_are_safe()
        async with self._repository.session.begin():
            if not await self._repository.update_packages(
                kodesi=kodesi,
                jumlah_koli=request.jumlah_koli,
                tanggal_kemas=request.tanggal_kemas,
            ):
                raise OperationalSaleNotFoundError(kodesi)
            await self._repository.add_audit(
                kodesi=kodesi,
                user_code=user_code,
                user_name=user_name,
                audit_status="UPDATE KOLI PENJUALAN",
            )
        return SalesOperationalUpdateResponse(
            kodesi=kodesi, status="packages_updated"
        )

    async def history(self, kodesi: str) -> SalesHistoryResponse:
        if not await self._repository.sale_exists(kodesi):
            raise OperationalSaleNotFoundError(kodesi)
        data = await self._repository.history(kodesi)
        return SalesHistoryResponse(kodesi=kodesi, **data)

    async def print_payload(
        self, *, kodesi: str, document_type: str
    ) -> SalesPrintPayload:
        data = await self._repository.print_data(
            kodesi=kodesi, document_type=document_type
        )
        if data is None:
            raise OperationalSaleNotFoundError(kodesi)
        return SalesPrintPayload(
            kodesi=kodesi, document_type=document_type, **data
        )

    async def whatsapp_payload(self, kodesi: str) -> SalesWhatsAppPayload:
        data = await self._repository.print_data(
            kodesi=kodesi, document_type="invoice"
        )
        if data is None:
            raise OperationalSaleNotFoundError(kodesi)
        settings = await self._repository.whatsapp_settings(kodesi)
        header = data["header"]
        lines = [settings.get("headerwa") or "INVOICE PENJUALAN"]
        company_name = data["company"].get("nama")
        if company_name:
            lines.append(str(company_name))
        lines.extend(
            [
                f"No: {kodesi}",
                f"Pelanggan: {header.get('namapelanggan') or '-'}",
                f"Tanggal: {header.get('tanggalfaktur') or '-'}",
                "Item:",
            ]
        )
        for item in data["items"]:
            quantity = item.get("qty") or 0
            lines.append(
                f"- {item.get('namabarang') or item.get('kodebarang')} "
                f"{quantity} {item.get('satuan') or ''}: "
                f"{item.get('hargatotal') or Decimal('0')}"
            )
        lines.append(f"Total: {header.get('total') or Decimal('0')}")
        if settings.get("footerwa"):
            lines.append(str(settings["footerwa"]))
        return SalesWhatsAppPayload(
            kodesi=kodesi,
            nomor_tujuan=settings.get("wa"),
            message="\n".join(lines),
            **data,
        )
