from datetime import datetime, timedelta
from decimal import Decimal, ROUND_HALF_UP
from uuid import uuid4

from sqlalchemy.exc import DBAPIError

from app.sales.command_repository import SalesCommandRepository
from app.sales.fifo_repository import SalesFifoRepository
from app.sales.fifo_service import SalesFifoService
from app.sales.schemas import (
    SalesCommandRequest,
    SalesCommandResponse,
    SalesDeleteResponse,
)


MONEY = Decimal("0.01")


class WriteApiDisabledError(Exception):
    pass


class NonTransactionalTablesError(Exception):
    def __init__(self, tables: list[str]) -> None:
        self.tables = tables


class SaleNotFoundError(Exception):
    pass


class SaleLockedError(Exception):
    pass


class ConcurrentUpdateError(Exception):
    pass


class SalesOrderLinkError(Exception):
    pass


class InvalidReferenceError(Exception):
    def __init__(self, references: list[str]) -> None:
        self.references = references


class SalesCommandService:
    def __init__(
        self,
        repository: SalesCommandRepository,
        *,
        write_enabled: bool,
        fifo_service: SalesFifoService | None = None,
    ) -> None:
        self._repository = repository
        self._write_enabled = write_enabled
        self._fifo_service = fifo_service or SalesFifoService(
            SalesFifoRepository(repository.session)
        )

    async def _ensure_writes_are_safe(self) -> None:
        if not self._write_enabled:
            raise WriteApiDisabledError()
        tables = await self._repository.non_transactional_tables()
        if tables:
            raise NonTransactionalTablesError(tables)
        await self._repository.session.rollback()

    @staticmethod
    def _money(value: Decimal) -> Decimal:
        return value.quantize(MONEY, rounding=ROUND_HALF_UP)

    def _details(self, request: SalesCommandRequest) -> list[dict[str, object]]:
        details: list[dict[str, object]] = []
        for index, item in enumerate(request.items, start=1):
            total = item.qty * item.harga_satuan
            for discount in (item.diskon1, item.diskon2, item.diskon3):
                total -= total * discount / Decimal("100")
            total = max(total - item.potongan, Decimal("0"))
            tax = (
                total * request.persen_ppn / Decimal("100")
                if item.ppn_flag == "Y"
                else Decimal("0")
            )
            details.append(
                {
                    **item.model_dump(),
                    "id": str(uuid4()),
                    "urutan": index,
                    "harga_total": self._money(total),
                    "pajak_detail": self._money(tax),
                    "ppn": item.ppn_flag,
                }
            )
        return details

    @staticmethod
    def _now() -> datetime:
        return datetime.now().replace(microsecond=0)

    @classmethod
    def _next_edit_time(cls, expected: datetime) -> datetime:
        expected = expected.replace(microsecond=0)
        return max(cls._now(), expected + timedelta(seconds=1))

    @staticmethod
    def _header_values(
        *,
        kodesi: str,
        request: SalesCommandRequest,
        now: datetime,
    ) -> dict[str, object]:
        return {
            "kodesi": kodesi,
            "now": now,
            "kode_pelanggan": request.kode_pelanggan,
            "kode_sales": request.kode_sales,
            "biaya": request.biaya_lain,
            "kode_akun_biaya": request.kode_akun_biaya,
            "kode_akun_pajak": request.kode_akun_pajak,
            "metode_ppn": request.metode_ppn,
            "persen_ppn": request.persen_ppn,
            "tanggal_faktur": request.tanggal_faktur,
            "tanggal_jatuh_tempo": request.tanggal_jatuh_tempo,
            "kode_akun_total": request.kode_akun_total,
            "kode_divisi": request.kode_divisi,
            "keterangan": request.keterangan,
            "kode_rute": request.kode_rute,
            "expected_tanggal_edit": request.expected_tanggal_edit,
        }

    async def create(
        self,
        *,
        request: SalesCommandRequest,
        user_code: str,
        user_name: str,
    ) -> SalesCommandResponse:
        await self._ensure_writes_are_safe()
        if request.expected_tanggal_edit is not None:
            raise ValueError("expected_tanggal_edit hanya untuk update")
        now = self._now()
        async with self._repository.session.begin():
            invalid = await self._repository.invalid_references(request)
            if invalid:
                raise InvalidReferenceError(invalid)
            period = request.tanggal_faktur.strftime("%y%m")
            kodesi = await self._repository.generate_kodesi(
                period=period, division=request.kode_divisi[-2:]
            )
            await self._repository.insert_header(
                self._header_values(kodesi=kodesi, request=request, now=now)
            )
            await self._repository.replace_details(
                kodesi=kodesi, details=self._details(request)
            )
            if request.kode_so and not await self._repository.link_sales_order(
                kodeso=request.kode_so, kodesi=kodesi
            ):
                raise SalesOrderLinkError(request.kode_so)
            await self._fifo_service.post_and_sync(
                kodesi=kodesi, user_code=user_code
            )
            await self._repository.add_audit(
                user_code=user_code,
                user_name=user_name,
                kodesi=kodesi,
                audit_status="SIMPAN PENJUALAN",
                edit_count=0,
                now=now,
            )
        return SalesCommandResponse(
            kodesi=kodesi,
            status="saved",
            fifo_status="posted",
            document_exists_before=False,
            tanggal_edit=now,
        )

    async def update(
        self,
        *,
        kodesi: str,
        request: SalesCommandRequest,
        user_code: str,
        user_name: str,
    ) -> SalesCommandResponse:
        await self._ensure_writes_are_safe()
        if request.expected_tanggal_edit is None:
            raise ConcurrentUpdateError()
        now = self._next_edit_time(request.expected_tanggal_edit)
        async with self._repository.session.begin():
            invalid = await self._repository.invalid_references(request)
            if invalid:
                raise InvalidReferenceError(invalid)
            current = await self._repository.lock_header(kodesi)
            if current is None:
                raise SaleNotFoundError(kodesi)
            if str(current["tutup"]) == "1":
                raise SaleLockedError(kodesi)
            values = self._header_values(
                kodesi=kodesi, request=request, now=now
            )
            if not await self._repository.update_header(values):
                raise ConcurrentUpdateError(kodesi)
            await self._repository.replace_details(
                kodesi=kodesi, details=self._details(request)
            )
            await self._repository.unlink_sales_orders(kodesi)
            if request.kode_so and not await self._repository.link_sales_order(
                kodeso=request.kode_so, kodesi=kodesi
            ):
                raise SalesOrderLinkError(request.kode_so)
            await self._fifo_service.post_and_sync(
                kodesi=kodesi, user_code=user_code
            )
            edit_count = await self._repository.next_edit_count(kodesi)
            await self._repository.add_audit(
                user_code=user_code,
                user_name=user_name,
                kodesi=kodesi,
                audit_status="EDIT PENJUALAN",
                edit_count=edit_count,
                now=now,
            )
        return SalesCommandResponse(
            kodesi=kodesi,
            status="updated",
            fifo_status="posted",
            document_exists_before=True,
            tanggal_edit=now,
        )

    async def delete(
        self, *, kodesi: str, user_code: str, user_name: str
    ) -> SalesDeleteResponse:
        await self._ensure_writes_are_safe()
        try:
            pairs = await self._fifo_service.get_document_pairs(kodesi)
            await self._repository.delete_via_procedure(
                kodesi=kodesi, user_code=user_code
            )
            await self._fifo_service.sync_balance_pairs(
                pairs=pairs, user_code=user_code
            )
            await self._repository.unlink_sales_order_details(kodesi)
            await self._repository.add_audit(
                user_code=user_code,
                user_name=user_name,
                kodesi=kodesi,
                audit_status="HAPUS PENJUALAN",
                edit_count=0,
                now=self._now(),
            )
            await self._repository.session.commit()
        except DBAPIError:
            await self._repository.session.rollback()
            raise
        return SalesDeleteResponse(
            kodesi=kodesi, status="deleted_via_fifo_procedure"
        )