"""
Billing service handling invoice calculation, lifecycle, and balance ledger.
"""

import asyncio
from datetime import datetime
from decimal import Decimal
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import AlreadyExistsException, NotFoundException, ValidationException
from app.core.money import npr
from app.core.security import hash_password
from app.models.billing import Bill, BillStatus
from app.models.order import OrderStatus
from app.repositories.bill_repository import BillRepository
from app.repositories.order_repository import OrderRepository
from app.repositories.patient_repository import PatientRepository
from app.schemas.billing import BillCreate, BillUpdate, BillVoidRequest
from app.services.discount_service import DiscountService


class BillingService:
    """Service handling invoicing, balance ledgers, and bill lifecycle."""

    def __init__(
        self,
        repository: BillRepository | None = None,
        discount_service: DiscountService | None = None,
        order_repo: OrderRepository | None = None,
    ) -> None:
        self.repo = repository or BillRepository()
        self.discount_service = discount_service or DiscountService()
        self.order_repo = order_repo or OrderRepository()
        self.patient_repo = PatientRepository()

    async def _issue_portal_pin(self, db: AsyncSession, patient_id: UUID) -> str:
        """Rotate the portal PIN and store hash + encrypted display copy together."""
        from app.core.portal_pin import encrypt_pin, generate_pin

        plain_pin = generate_pin()
        patient = await self.patient_repo.get_by_id(db, patient_id)
        if patient:
            patient.pin_hash = await asyncio.to_thread(hash_password, plain_pin)
            patient.portal_pin_enc = encrypt_pin(plain_pin)
            await db.flush()
        return plain_pin

    async def create_bill(
        self,
        db: AsyncSession,
        lab_id: UUID,
        data: BillCreate,
    ) -> tuple[Bill, str]:
        """
        Generate a 1:1 invoice for a VisitOrder.

        Enforces:
        1. Order exists and belongs to the specified lab branch.
        2. Order does not already have an active bill (409 Conflict via AlreadyExistsException).
        3. Subtotal computed from immutable snapshot price_at_order on order_tests and order_packages.
        4. Discount applied from DiscountRule (if rule_id) or manual discount.
        5. Total amount clamped to >= 0.00.
        6. Status initialized to PAID (if total == 0) or UNPAID.
        7. Sequential invoice number assigned (INV{YY}{6-digit-seq}).
        """
        # 1. Fetch Order with items
        order = await self.order_repo.get_by_id(db, data.order_id)
        if not order or order.lab_id != lab_id:
            raise NotFoundException(f"Visit order '{data.order_id}' was not found in this branch.")

        # 2. Enforce 1:1 bill per order
        existing_bill = await self.repo.get_by_order_id(db, data.order_id, lab_id)
        if existing_bill and existing_bill.status != BillStatus.VOID:
            msg = (
                f"An active bill ('{existing_bill.bill_number}') "
                f"already exists for order '{order.order_number}'."
            )
            raise AlreadyExistsException(msg)

        # 3. Compute Subtotal from snapshot prices
        test_subtotal = sum((ot.price_at_order for ot in order.order_tests), Decimal("0.00"))
        pkg_subtotal = sum((op.price_at_order for op in order.order_packages), Decimal("0.00"))
        subtotal = test_subtotal + pkg_subtotal

        # 4. Compute Discount (strictly validate and clamp: 0.00 <= discount_amount <= subtotal)
        discount_amount = Decimal("0.00")
        discount_rule_id = None

        if data.discount_rule_id:
            rule = await self.discount_service.get_discount_rule(db, data.discount_rule_id, lab_id)
            discount_amount = self.discount_service.calculate_discount(subtotal, rule)
            discount_rule_id = rule.id
        elif data.manual_discount_amount:
            discount_amount = npr(min(data.manual_discount_amount, subtotal))

        # Enforce invariant: discount can never exceed subtotal or be negative
        if discount_amount > subtotal:
            discount_amount = subtotal
        if discount_amount < Decimal("0.00"):
            discount_amount = Decimal("0.00")

        # 5. Compute Taxes & Totals (all quantized to paisa)
        tax_amount = npr(data.tax_amount)
        total_amount = npr(max(Decimal("0.00"), subtotal - discount_amount + tax_amount))
        if subtotal <= Decimal("0.00") and total_amount > Decimal("0.00"):
            raise ValidationException(
                "This visit order has no billable tests (subtotal is zero). "
                "Add priced tests before invoicing."
            )

        paid_amount = Decimal("0.00")
        due_amount = npr(total_amount)
        status = BillStatus.PAID if total_amount == Decimal("0.00") else BillStatus.UNPAID

        # 7. Invoice Numbering
        bill_number = await self.repo.get_next_invoice_number(db, lab_id)

        bill = Bill(
            lab_id=lab_id,
            order_id=order.id,
            patient_id=order.patient_id,
            discount_rule_id=discount_rule_id,
            bill_number=bill_number,
            status=status,
            subtotal=subtotal,
            discount_amount=discount_amount,
            tax_amount=tax_amount,
            total_amount=total_amount,
            paid_amount=paid_amount,
            due_amount=due_amount,
            notes=data.notes,
        )

        created = await self.repo.create(db, bill)
        if order.status == OrderStatus.PENDING:
            order.status = OrderStatus.CONFIRMED
            await db.flush()

        if order.appointment_id:
            from app.models.appointment import Appointment, AppointmentStatus

            appointment = await db.get(Appointment, order.appointment_id)
            if appointment and appointment.status != AppointmentStatus.COMPLETED:
                appointment.status = AppointmentStatus.COMPLETED
                await db.flush()

        portal_pin = await self._issue_portal_pin(db, order.patient_id)
        return created, portal_pin

    async def get_bill(self, db: AsyncSession, bill_id: UUID, lab_id: UUID | None = None) -> Bill:
        """Fetch bill by ID with optional branch scoping."""
        bill = await self.repo.get_by_id(db, bill_id, lab_id)
        if not bill:
            raise NotFoundException(f"Bill with ID '{bill_id}' was not found.")
        return bill

    async def get_bill_by_order(self, db: AsyncSession, order_id: UUID, lab_id: UUID | None = None) -> Bill:
        """Fetch bill for a visit order."""
        bill = await self.repo.get_by_order_id(db, order_id, lab_id)
        if not bill:
            raise NotFoundException(f"No bill found for visit order '{order_id}'.")
        return bill

    async def list_bills(
        self,
        db: AsyncSession,
        lab_id: UUID,
        status: BillStatus | None = None,
        patient_id: UUID | None = None,
        order_id: UUID | None = None,
        has_due: bool | None = None,
        is_abroad: bool | None = None,
        destination_country: str | None = None,
        search: str | None = None,
        page: int = 1,
        limit: int = 50,
        created_from: datetime | None = None,
        created_to: datetime | None = None,
    ) -> tuple[list[Bill], int]:
        """List paginated bills."""
        offset = (page - 1) * limit
        return await self.repo.list_bills(
            db,
            lab_id=lab_id,
            status=status,
            patient_id=patient_id,
            order_id=order_id,
            has_due=has_due,
            is_abroad=is_abroad,
            destination_country=destination_country,
            search=search,
            offset=offset,
            limit=limit,
            created_from=created_from,
            created_to=created_to,
        )

    async def update_bill(
        self,
        db: AsyncSession,
        bill_id: UUID,
        lab_id: UUID,
        data: BillUpdate,
    ) -> Bill:
        """Update bill notes."""
        bill = await self.get_bill(db, bill_id, lab_id)
        if bill.status == BillStatus.VOID:
            raise ValidationException("Cannot update notes on a voided bill.")
        return await self.repo.update(db, bill, data.notes)

    async def void_bill(
        self,
        db: AsyncSession,
        bill_id: UUID,
        lab_id: UUID,
        data: BillVoidRequest,
    ) -> Bill:
        """
        Void an invoice.

        Enforces:
        - Cannot void an already voided bill.
        - Cannot void a bill with recorded payments (paid_amount > 0).
        """
        bill = await self.get_bill(db, bill_id, lab_id)
        if bill.status == BillStatus.VOID:
            raise ValidationException("Bill is already voided.")

        if bill.paid_amount > Decimal("0.00"):
            msg = (
                f"Cannot void bill '{bill.bill_number}' because payments of {bill.paid_amount} "
                "have been recorded. Refund payments first."
            )
            raise ValidationException(msg)

        return await self.repo.void_bill(db, bill, data.reason)
