from datetime import date, datetime
from decimal import Decimal, ROUND_HALF_UP
from typing import Any
from uuid import uuid4

from app.sales.order_repository import SalesOrderRepository
from app.sales.schemas import (
    Pagination,
    SalesOrderDetailResponse,
    SalesOrderDraft,
    SalesOrderHistoryItem,
    SalesOrderListItem,
    SalesOrderListResponse,
    SalesOrderCommandRequest,
    SalesOrderCommandResponse,
    SalesOrderDeleteResponse,
    SalesOrderNumberPreviewResponse,
    SalesShortageOrderItem,
    SalesShortageOrderRequest,
    SalesShortageOrderResponse,
)


MONEY = Decimal("0.01")


class OrderWriteDisabledError(Exception):
    pass


class OrderNonTransactionalTablesError(Exception):
    def __init__(self, tables: list[str]) -> None:
        self.tables = tables


class NoStockShortageError(Exception):
    pass


class InvalidOrderReferenceError(Exception):
    def __init__(self, references: list[str]) -> None:
        self.references = references


class OrderPeriodClosedError(Exception):
    pass


class OrderNotFoundError(Exception):
    pass


class OrderLockedError(Exception):
    pass


class OrderAlreadyProcessedError(Exception):
    def __init__(self, kodesi: str | None = None) -> None:
        self.kodesi = kodesi


class OrderCreditLimitExceededError(Exception):
    def __init__(
        self, limit: Decimal, current_piutang: Decimal, requested_total: Decimal
    ) -> None:
        self.limit = limit
        self.current_piutang = current_piutang
        self.requested_total = requested_total


class OrderConcurrentUpdateError(Exception):
    pass


class SalesOrderService:
    def __init__(
        self,
        repository: SalesOrderRepository,
        *,
        write_enabled: bool = False,
    ) -> None:
        self._repository = repository
        self._write_enabled = write_enabled

    @staticmethod
    def _money(value: Decimal) -> Decimal:
        return value.quantize(MONEY, rounding=ROUND_HALF_UP)

    async def list_orders(
        self,
        *,
        month: int,
        year: int,
        search: str | None = None,
        only_unlocked: bool = False,
        kode_sales: str | None = None,
        page: int = 1,
        page_size: int = 25,
    ) -> SalesOrderListResponse:
        rows, total = await self._repository.list_orders(
            month=month,
            year=year,
            search=search,
            only_unlocked=only_unlocked,
            kode_sales=kode_sales,
            limit=page_size,
            offset=(page - 1) * page_size,
        )
        return SalesOrderListResponse(
            data=[SalesOrderListItem.model_validate(row) for row in rows],
            pagination=Pagination(
                page=page,
                page_size=page_size,
                total=total,
                total_pages=(total + page_size - 1) // page_size,
            ),
        )

    async def get_order(
        self, kodeso: str, *, kode_sales: str | None = None
    ) -> SalesOrderDetailResponse | None:
        row = await self._repository.get_order(kodeso, kode_sales=kode_sales)
        return SalesOrderDetailResponse.model_validate(row) if row else None

    async def preview_number(
        self, *, period: str, division: str
    ) -> SalesOrderNumberPreviewResponse:
        prefix = f"SO{period}{division.zfill(2)}"
        kodeso = await self._repository.generate_kodeso(
            period=period, division=division
        )
        seq = int(kodeso[-6:])
        return SalesOrderNumberPreviewResponse(
            periode=period,
            kode_divisi=division,
            prefix=prefix,
            nomor_preview=kodeso,
            sequence=seq,
        )

    async def get_history(self, kodeso: str) -> list[SalesOrderHistoryItem]:
        rows = await self._repository.get_order_history(kodeso)
        return [SalesOrderHistoryItem.model_validate(r) for r in rows]

    async def get_for_sales(
        self, kodeso: str, *, kode_sales: str | None = None
    ) -> SalesOrderDraft | None:
        row = await self._repository.get_for_sales(kodeso, kode_sales=kode_sales)
        return SalesOrderDraft.model_validate(row) if row else None

    async def create_order(
        self,
        *,
        request: SalesOrderCommandRequest,
        user_code: str,
        user_name: str,
    ) -> SalesOrderCommandResponse:
        if not self._write_enabled:
            raise OrderWriteDisabledError()
        tables = await self._repository.non_transactional_tables()
        if tables:
            raise OrderNonTransactionalTablesError(tables)
        await self._repository.session.rollback()

        async with self._repository.session.begin():
            invalid = await self._repository.invalid_command_references(
                kode_pelanggan=request.kode_pelanggan,
                items=request.items,
            )
            if invalid:
                raise InvalidOrderReferenceError(invalid)

            order_date = request.tanggal_order or date.today()
            if await self._repository.is_period_closed(
                month=order_date.month, year=order_date.year
            ):
                raise OrderPeriodClosedError()

            details: list[dict[str, Any]] = []
            subtotal = Decimal("0")
            total_tax = Decimal("0")

            for index, item in enumerate(request.items, start=1):
                line_total = item.qty * item.harga_satuan
                for d in (item.diskon1, item.diskon2, item.diskon3):
                    line_total -= line_total * d / Decimal("100")
                line_total = max(line_total - item.potongan, Decimal("0"))
                line_tax = (
                    line_total * request.persen_ppn / Decimal("100")
                    if item.ppn_flag == "Y"
                    else Decimal("0")
                )
                line_total_money = self._money(line_total)
                line_tax_money = self._money(line_tax)
                subtotal += line_total_money
                total_tax += line_tax_money

                details.append(
                    {
                        "kodeso": "",
                        "kode_barang": item.kode_barang,
                        "qty": item.qty,
                        "satuan": item.satuan,
                        "harga_satuan": self._money(item.harga_satuan),
                        "diskon1": item.diskon1,
                        "diskon2": item.diskon2,
                        "diskon3": item.diskon3,
                        "potongan": self._money(item.potongan),
                        "harga_total": line_total_money,
                        "ppn": item.ppn_flag,
                        "pajak_detail": line_tax_money,
                        "kode_akun_penjualan": item.kode_akun_penjualan,
                        "kode_akun_hpp": item.kode_akun_hpp,
                        "kode_akun_persediaan": item.kode_akun_persediaan,
                        "konversi": item.konversi,
                        "harga_beli_konversi": self._money(item.harga_beli_konversi),
                        "kode_lokasi": item.kode_lokasi,
                        "id": str(uuid4()),
                        "urutan": index,
                    }
                )

            grand_total = subtotal + total_tax + self._money(request.biaya_lain)

            credit = await self._repository.get_customer_credit(request.kode_pelanggan)
            if credit:
                limit_piutang = Decimal(str(credit["limit_piutang"]))
                current_piutang = Decimal(str(credit["piutang"]))
                if limit_piutang > 0 and (current_piutang + grand_total) > limit_piutang:
                    raise OrderCreditLimitExceededError(
                        limit=limit_piutang,
                        current_piutang=current_piutang,
                        requested_total=grand_total,
                    )

            period = order_date.strftime("%y%m")
            division = request.kode_divisi[-2:]
            kodeso = await self._repository.generate_kodeso(
                period=period, division=division
            )
            for d in details:
                d["kodeso"] = kodeso

            now = datetime.now().replace(microsecond=0)
            header = {
                "kodeso": kodeso,
                "kode_pelanggan": request.kode_pelanggan,
                "biaya": self._money(request.biaya_lain),
                "subtotal": self._money(subtotal),
                "pajak": self._money(total_tax),
                "metode_ppn": request.metode_ppn,
                "persen_ppn": request.persen_ppn,
                "total": self._money(grand_total),
                "catatan": request.catatan,
                "keterangan": request.keterangan,
                "now": now,
            }

            await self._repository.create_order(
                header=header,
                details=details,
                user_code=user_code,
                user_name=user_name,
            )

            return SalesOrderCommandResponse(
                kodeso=kodeso,
                status="saved",
                document_exists_before=False,
                tanggal_edit=now,
            )

    async def update_order(
        self,
        *,
        kodeso: str,
        request: SalesOrderCommandRequest,
        user_code: str,
        user_name: str,
    ) -> SalesOrderCommandResponse:
        if not self._write_enabled:
            raise OrderWriteDisabledError()
        tables = await self._repository.non_transactional_tables()
        if tables:
            raise OrderNonTransactionalTablesError(tables)
        await self._repository.session.rollback()

        async with self._repository.session.begin():
            current = await self._repository.get_order_state(kodeso)
            if not current:
                raise OrderNotFoundError()
            if current["tutup"]:
                raise OrderLockedError()
            if current["kodesi"]:
                raise OrderAlreadyProcessedError(current["kodesi"])
            if request.expected_tanggal_edit is not None:
                if current["tanggal_edit"] != request.expected_tanggal_edit:
                    raise OrderConcurrentUpdateError()

            invalid = await self._repository.invalid_command_references(
                kode_pelanggan=request.kode_pelanggan,
                items=request.items,
            )
            if invalid:
                raise InvalidOrderReferenceError(invalid)

            order_date = request.tanggal_order or (
                current["tanggal_created"].date()
                if hasattr(current["tanggal_created"], "date")
                else date.today()
            )
            if await self._repository.is_period_closed(
                month=order_date.month, year=order_date.year
            ):
                raise OrderPeriodClosedError()

            details: list[dict[str, Any]] = []
            subtotal = Decimal("0")
            total_tax = Decimal("0")

            for index, item in enumerate(request.items, start=1):
                line_total = item.qty * item.harga_satuan
                for d in (item.diskon1, item.diskon2, item.diskon3):
                    line_total -= line_total * d / Decimal("100")
                line_total = max(line_total - item.potongan, Decimal("0"))
                line_tax = (
                    line_total * request.persen_ppn / Decimal("100")
                    if item.ppn_flag == "Y"
                    else Decimal("0")
                )
                line_total_money = self._money(line_total)
                line_tax_money = self._money(line_tax)
                subtotal += line_total_money
                total_tax += line_tax_money

                details.append(
                    {
                        "kodeso": kodeso,
                        "kode_barang": item.kode_barang,
                        "qty": item.qty,
                        "satuan": item.satuan,
                        "harga_satuan": self._money(item.harga_satuan),
                        "diskon1": item.diskon1,
                        "diskon2": item.diskon2,
                        "diskon3": item.diskon3,
                        "potongan": self._money(item.potongan),
                        "harga_total": line_total_money,
                        "ppn": item.ppn_flag,
                        "pajak_detail": line_tax_money,
                        "kode_akun_penjualan": item.kode_akun_penjualan,
                        "kode_akun_hpp": item.kode_akun_hpp,
                        "kode_akun_persediaan": item.kode_akun_persediaan,
                        "konversi": item.konversi,
                        "harga_beli_konversi": self._money(item.harga_beli_konversi),
                        "kode_lokasi": item.kode_lokasi,
                        "id": str(uuid4()),
                        "urutan": index,
                    }
                )

            grand_total = subtotal + total_tax + self._money(request.biaya_lain)
            current_total = Decimal(str(current["total"]))
            net_increase = grand_total - current_total

            if net_increase > 0:
                credit = await self._repository.get_customer_credit(request.kode_pelanggan)
                if credit:
                    limit_piutang = Decimal(str(credit["limit_piutang"]))
                    current_piutang = Decimal(str(credit["piutang"]))
                    if limit_piutang > 0 and (current_piutang + net_increase) > limit_piutang:
                        raise OrderCreditLimitExceededError(
                            limit=limit_piutang,
                            current_piutang=current_piutang,
                            requested_total=grand_total,
                        )

            edit_count = await self._repository.get_order_audit_count(kodeso)
            now = datetime.now().replace(microsecond=0)
            header = {
                "kode_pelanggan": request.kode_pelanggan,
                "biaya": self._money(request.biaya_lain),
                "subtotal": self._money(subtotal),
                "pajak": self._money(total_tax),
                "metode_ppn": request.metode_ppn,
                "persen_ppn": request.persen_ppn,
                "total": self._money(grand_total),
                "catatan": request.catatan,
                "keterangan": request.keterangan,
                "now": now,
            }

            await self._repository.update_order(
                kodeso=kodeso,
                header=header,
                details=details,
                user_code=user_code,
                user_name=user_name,
                edit_count=edit_count,
            )

            return SalesOrderCommandResponse(
                kodeso=kodeso,
                status="updated",
                document_exists_before=True,
                tanggal_edit=now,
            )

    async def delete_order(
        self,
        *,
        kodeso: str,
        user_code: str,
        user_name: str,
    ) -> SalesOrderDeleteResponse:
        if not self._write_enabled:
            raise OrderWriteDisabledError()
        tables = await self._repository.non_transactional_tables()
        if tables:
            raise OrderNonTransactionalTablesError(tables)
        await self._repository.session.rollback()

        async with self._repository.session.begin():
            current = await self._repository.get_order_state(kodeso)
            if not current:
                raise OrderNotFoundError()
            if current["tutup"]:
                raise OrderLockedError()
            if current["kodesi"]:
                raise OrderAlreadyProcessedError(current["kodesi"])

            order_date = (
                current["tanggal_created"].date()
                if hasattr(current["tanggal_created"], "date")
                else date.today()
            )
            if await self._repository.is_period_closed(
                month=order_date.month, year=order_date.year
            ):
                raise OrderPeriodClosedError()

            await self._repository.delete_order(
                kodeso=kodeso,
                user_code=user_code,
                user_name=user_name,
            )

            return SalesOrderDeleteResponse(kodeso=kodeso, status="deleted")

    async def create_from_shortage(
        self,
        *,
        request: SalesShortageOrderRequest,
        user_code: str,
        user_name: str,
    ) -> SalesShortageOrderResponse:
        if not self._write_enabled:
            raise OrderWriteDisabledError()
        tables = await self._repository.non_transactional_tables()
        if tables:
            raise OrderNonTransactionalTablesError(tables)
        await self._repository.session.rollback()

        async with self._repository.session.begin():
            invalid = await self._repository.invalid_references(request)
            if invalid:
                raise InvalidOrderReferenceError(invalid)
            pairs = list(
                {
                    (item.kode_barang, item.kode_lokasi or "")
                    for item in request.items
                }
            )
            balances = await self._repository.lock_and_get_fifo_balances(
                [
                    {"kodebarang": product, "kodelokasi": location}
                    for product, location in sorted(pairs)
                ]
            )
            details: list[dict[str, object]] = []
            response_items: list[SalesShortageOrderItem] = []
            for item in request.items:
                location = item.kode_lokasi or ""
                key = (item.kode_barang, location)
                available = Decimal(str(balances.get(key, 0)))
                required = item.qty * item.konversi
                shortage_base = max(required - available, Decimal("0"))
                balances[key] = max(available - required, Decimal("0"))
                if shortage_base == 0:
                    continue
                shortage_qty = shortage_base / item.konversi
                proportional_discount = (
                    item.potongan * shortage_qty / item.qty
                )
                total = shortage_qty * item.harga_satuan
                for discount in (item.diskon1, item.diskon2, item.diskon3):
                    total -= total * discount / Decimal("100")
                total = max(total - proportional_discount, Decimal("0"))
                tax = (
                    total * request.persen_ppn / Decimal("100")
                    if item.ppn_flag == "Y"
                    else Decimal("0")
                )
                response_items.append(
                    SalesShortageOrderItem(
                        kode_barang=item.kode_barang,
                        kode_lokasi=location,
                        qty_order=shortage_qty,
                        satuan=item.satuan,
                    )
                )
                details.append(
                    {
                        "kode_barang": item.kode_barang,
                        "qty": shortage_qty,
                        "satuan": item.satuan,
                        "harga_satuan": item.harga_satuan,
                        "diskon1": item.diskon1,
                        "diskon2": item.diskon2,
                        "diskon3": item.diskon3,
                        "potongan": proportional_discount,
                        "harga_total": total,
                        "ppn": item.ppn_flag,
                        "pajak_detail": tax,
                        "kode_akun_penjualan": item.kode_akun_penjualan,
                        "kode_akun_hpp": item.kode_akun_hpp,
                        "kode_akun_persediaan": item.kode_akun_persediaan,
                        "konversi": item.konversi,
                        "harga_beli_konversi": item.harga_beli_konversi,
                        "kode_lokasi": location,
                        "id": str(uuid4()),
                        "urutan": len(details) + 1,
                    }
                )
            if not details:
                raise NoStockShortageError()
            subtotal = sum(
                (Decimal(str(item["harga_total"])) for item in details),
                Decimal("0"),
            )
            pajak = sum(
                (Decimal(str(item["pajak_detail"])) for item in details),
                Decimal("0"),
            )
            now = datetime.now().replace(microsecond=0)
            kodeso = await self._repository.generate_kodeso(
                period=request.tanggal_order.strftime("%y%m"),
                division=request.kode_divisi[-2:],
            )
            await self._repository.insert_shortage_order(
                header={
                    "kodeso": kodeso,
                    "now": now,
                    "kode_pelanggan": request.kode_pelanggan,
                    "biaya": request.biaya_lain,
                    "subtotal": subtotal,
                    "pajak": pajak,
                    "metode_ppn": request.metode_ppn,
                    "persen_ppn": request.persen_ppn,
                    "total": subtotal + pajak + request.biaya_lain,
                    "catatan": "AUTO ORDER DARI KEKURANGAN STOK",
                    "keterangan": request.keterangan,
                },
                details=[{**row, "kodeso": kodeso} for row in details],
            )
            await self._repository.add_audit(
                kodeso=kodeso,
                user_code=user_code,
                user_name=user_name,
            )
        return SalesShortageOrderResponse(
            kodeso=kodeso,
            status="created_from_shortage",
            item_count=len(response_items),
            items=response_items,
        )
