from datetime import datetime
from decimal import Decimal

from app.application.company_access import resolve_company_for_user
from app.application.exceptions import ConflictError, NotFoundError, ValidationError
from app.application.services.voucher_service import VoucherService
from app.domain.entities.chart_of_account import ChartOfAccount
from app.domain.entities.company import Company
from app.domain.entities.inventory import InventoryBalance, InventoryTransaction
from app.domain.entities.sales import (
    Customer,
    SalesDelivery,
    SalesDeliveryLine,
    SalesInvoice,
    SalesInvoiceLine,
    SalesOrder,
    SalesOrderLine,
    SalesPayment,
    SalesPaymentAllocation,
)
from app.domain.entities.user_registration import UserRegistration
from app.domain.enums import (
    AccountType,
    CustomerBusinessType,
    InventoryTxnType,
    PaymentTerms,
    PreferredPaymentMethod,
    SalesDeliveryStatus,
    SalesInvoiceStatus,
    SalesOrderStatus,
    SalesPaymentStatus,
    VoucherType,
    account_nature_for_type,
)
from app.domain.repositories.chart_of_account_repository import ChartOfAccountRepository
from app.domain.repositories.company_repository import CompanyRepository
from app.domain.repositories.inventory_repository import InventoryRepository
from app.domain.repositories.sales_repository import SalesRepository


class SalesService:
    def __init__(
        self,
        sales_repository: SalesRepository,
        inventory_repository: InventoryRepository,
        account_repository: ChartOfAccountRepository,
        company_repository: CompanyRepository,
        voucher_service: VoucherService | None = None,
    ) -> None:
        self._sales = sales_repository
        self._inventory = inventory_repository
        self._accounts = account_repository
        self._companies = company_repository
        self._vouchers = voucher_service

    _PAYMENT_TERMS_DAYS = {
        PaymentTerms.COD: 0,
        PaymentTerms.NET_15: 15,
        PaymentTerms.NET_30: 30,
        PaymentTerms.NET_45: 45,
        PaymentTerms.NET_60: 60,
        PaymentTerms.ADVANCE: 0,
    }

    # ---- Customers ----
    async def create_customer(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> Customer:
        await self._get_user_company(user, company_id)
        code = (data.get("code") or "").strip() or await self._sales.get_next_customer_code(
            company_id
        )
        if await self._sales.get_customer_by_code(company_id, code):
            raise ConflictError(f"Customer code '{code}' already exists")

        warehouse_id = data.get("default_warehouse_id") or None
        if warehouse_id:
            warehouse = await self._inventory.get_warehouse(warehouse_id, company_id)
            if not warehouse:
                raise NotFoundError("Default warehouse not found")

        payment_terms = data.get("payment_terms")
        if isinstance(payment_terms, str):
            payment_terms = PaymentTerms(payment_terms)
        payment_terms_days = data.get("payment_terms_days", 30)
        if payment_terms and "payment_terms_days" not in data:
            payment_terms_days = self._PAYMENT_TERMS_DAYS.get(payment_terms, 30)

        business_type = data.get("business_type")
        if isinstance(business_type, str):
            business_type = CustomerBusinessType(business_type)
        preferred_payment_method = data.get("preferred_payment_method")
        if isinstance(preferred_payment_method, str):
            preferred_payment_method = PreferredPaymentMethod(preferred_payment_method)

        address_line1 = data["address_line1"]
        opening_balance = Decimal(str(data.get("opening_balance", 0)))
        account_id = await self._resolve_or_create_customer_ar_account(
            company_id=company_id,
            preferred_account_id=data.get("account_id") or None,
            customer_code=code,
            customer_name=data["name"],
            opening_balance=opening_balance,
        )

        customer = Customer(
            company_id=company_id,
            code=code,
            name=data["name"],
            business_type=business_type,
            contact_person=data["contact_person"],
            designation=data.get("designation"),
            phone=data["phone"],
            alternate_phone=data.get("alternate_phone"),
            email=data.get("email"),
            website=data.get("website"),
            ntn=data.get("ntn"),
            sales_tax_no=data.get("sales_tax_no"),
            address_line1=address_line1,
            address_line2=data.get("address_line2"),
            city=data["city"],
            state_province=data.get("state_province"),
            country=data.get("country") or "Pakistan",
            postal_code=data.get("postal_code"),
            notes=data.get("notes"),
            payment_terms=payment_terms,
            payment_terms_days=payment_terms_days,
            credit_limit=Decimal(str(data.get("credit_limit", 0))),
            opening_balance=opening_balance,
            currency=data.get("currency") or "PKR",
            so_prefix=data.get("so_prefix"),
            default_warehouse_id=warehouse_id,
            preferred_payment_method=preferred_payment_method,
            logo=data.get("logo"),
            address=address_line1,
            account_id=account_id,
            is_active=data.get("is_active", True),
        )
        return await self._sales.create_customer(customer)

    async def get_customer(
        self, user: UserRegistration, company_id: str, customer_id: str
    ) -> Customer:
        await self._get_user_company(user, company_id)
        customer = await self._sales.get_customer(customer_id, company_id)
        if not customer:
            raise NotFoundError("Customer not found")
        return customer

    async def list_customers(
        self,
        user: UserRegistration,
        company_id: str,
        is_active: bool | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[Customer], int]:
        await self._get_user_company(user, company_id)
        items = await self._sales.list_customers(company_id, is_active, skip, limit)
        total = await self._sales.count_customers(company_id, is_active)
        return items, total

    async def update_customer(
        self, user: UserRegistration, company_id: str, customer_id: str, data: dict
    ) -> Customer:
        customer = await self.get_customer(user, company_id, customer_id)
        if "code" in data and data["code"] and data["code"] != customer.code:
            if await self._sales.get_customer_by_code(company_id, data["code"]):
                raise ConflictError(f"Customer code '{data['code']}' already exists")
            customer.code = data["code"]
        if "default_warehouse_id" in data:
            warehouse_id = data["default_warehouse_id"] or None
            if warehouse_id:
                warehouse = await self._inventory.get_warehouse(warehouse_id, company_id)
                if not warehouse:
                    raise NotFoundError("Default warehouse not found")
            customer.default_warehouse_id = warehouse_id
        if "business_type" in data:
            value = data["business_type"]
            customer.business_type = CustomerBusinessType(value) if value else None
        if "payment_terms" in data:
            value = data["payment_terms"]
            customer.payment_terms = PaymentTerms(value) if value else None
            if customer.payment_terms and "payment_terms_days" not in data:
                customer.payment_terms_days = self._PAYMENT_TERMS_DAYS.get(
                    customer.payment_terms, customer.payment_terms_days
                )
        if "preferred_payment_method" in data:
            value = data["preferred_payment_method"]
            customer.preferred_payment_method = (
                PreferredPaymentMethod(value) if value else None
            )
        for field in (
            "name",
            "contact_person",
            "designation",
            "phone",
            "alternate_phone",
            "email",
            "website",
            "ntn",
            "sales_tax_no",
            "address_line1",
            "address_line2",
            "city",
            "state_province",
            "country",
            "postal_code",
            "notes",
            "payment_terms_days",
            "currency",
            "so_prefix",
            "logo",
            "account_id",
            "is_active",
        ):
            if field in data:
                setattr(customer, field, data[field])
        if "credit_limit" in data:
            customer.credit_limit = Decimal(str(data["credit_limit"]))
        if "opening_balance" in data:
            customer.opening_balance = Decimal(str(data["opening_balance"]))
        if "address_line1" in data:
            customer.address = data["address_line1"]

        customer.account_id = await self._resolve_or_create_customer_ar_account(
            company_id=company_id,
            preferred_account_id=customer.account_id,
            customer_code=customer.code,
            customer_name=customer.name,
            opening_balance=customer.opening_balance or Decimal("0"),
        )

        customer.updated_at = datetime.utcnow()
        updated = await self._sales.update_customer(customer_id, customer)
        if not updated:
            raise NotFoundError("Customer not found")
        return updated

    async def delete_customer(
        self, user: UserRegistration, company_id: str, customer_id: str
    ) -> None:
        await self.get_customer(user, company_id, customer_id)
        if not await self._sales.delete_customer(customer_id):
            raise NotFoundError("Customer not found")

    # ---- Sales orders ----
    async def create_sales_order(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> SalesOrder:
        await self._get_user_company(user, company_id)
        customer = await self._sales.get_customer(data["customer_id"], company_id)
        if not customer:
            raise NotFoundError("Customer not found")

        warehouse_id = data.get("warehouse_id") or customer.default_warehouse_id or None
        if warehouse_id:
            warehouse = await self._inventory.get_warehouse(warehouse_id, company_id)
            if not warehouse:
                raise NotFoundError("Warehouse not found")

        lines = await self._build_so_lines(company_id, data.get("lines") or [])
        subtotal, tax_amount, total_amount = self._sum_so_totals(lines)
        so_number = await self._sales.get_next_so_number(company_id)

        payment_terms = data.get("payment_terms") or customer.payment_terms
        if isinstance(payment_terms, str):
            payment_terms = PaymentTerms(payment_terms)

        order = SalesOrder(
            company_id=company_id,
            so_number=so_number,
            customer_id=data["customer_id"],
            warehouse_id=warehouse_id,
            order_date=data["order_date"],
            delivery_date=data.get("delivery_date"),
            delivery_address=data.get("delivery_address"),
            delivery_contact=data.get("delivery_contact"),
            delivery_phone=data.get("delivery_phone"),
            payment_terms=payment_terms,
            currency=data.get("currency") or customer.currency or "PKR",
            notes=data.get("notes"),
            status=SalesOrderStatus.DRAFT,
            subtotal=subtotal,
            tax_amount=tax_amount,
            total_amount=total_amount,
            created_by=user.id,
            lines=lines,
        )
        return await self._sales.create_sales_order(order)

    async def get_sales_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> SalesOrder:
        await self._get_user_company(user, company_id)
        order = await self._sales.get_sales_order(order_id, company_id)
        if not order:
            raise NotFoundError("Sales order not found")
        return order

    async def list_sales_orders(
        self,
        user: UserRegistration,
        company_id: str,
        status: SalesOrderStatus | None = None,
        customer_id: str | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[SalesOrder], int]:
        await self._get_user_company(user, company_id)
        items = await self._sales.list_sales_orders(
            company_id, status, customer_id, skip, limit
        )
        total = await self._sales.count_sales_orders(company_id, status, customer_id)
        return items, total

    async def update_sales_order(
        self, user: UserRegistration, company_id: str, order_id: str, data: dict
    ) -> SalesOrder:
        order = await self.get_sales_order(user, company_id, order_id)
        if order.status != SalesOrderStatus.DRAFT:
            raise ValidationError("Only draft sales orders can be updated")

        if "customer_id" in data and data["customer_id"]:
            customer = await self._sales.get_customer(data["customer_id"], company_id)
            if not customer:
                raise NotFoundError("Customer not found")
            order.customer_id = data["customer_id"]
        if "warehouse_id" in data:
            warehouse_id = data["warehouse_id"] or None
            if warehouse_id:
                warehouse = await self._inventory.get_warehouse(warehouse_id, company_id)
                if not warehouse:
                    raise NotFoundError("Warehouse not found")
            order.warehouse_id = warehouse_id
        if "payment_terms" in data:
            value = data["payment_terms"]
            order.payment_terms = PaymentTerms(value) if value else None
        for field in (
            "order_date",
            "delivery_date",
            "delivery_address",
            "delivery_contact",
            "delivery_phone",
            "currency",
            "notes",
        ):
            if field in data:
                setattr(order, field, data[field])

        replace_lines = False
        if "lines" in data and data["lines"] is not None:
            order.lines = await self._build_so_lines(company_id, data["lines"])
            subtotal, tax_amount, total_amount = self._sum_so_totals(order.lines)
            order.subtotal = subtotal
            order.tax_amount = tax_amount
            order.total_amount = total_amount
            replace_lines = True

        order.updated_at = datetime.utcnow()
        updated = await self._sales.update_sales_order(
            order_id, order, replace_lines=replace_lines
        )
        if not updated:
            raise NotFoundError("Sales order not found")
        return updated

    async def confirm_sales_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> SalesOrder:
        order = await self.get_sales_order(user, company_id, order_id)
        if order.status != SalesOrderStatus.DRAFT:
            raise ValidationError("Only draft sales orders can be confirmed")
        if not order.lines:
            raise ValidationError("Sales order must have at least one line")
        order.status = SalesOrderStatus.CONFIRMED
        order.confirmed_at = datetime.utcnow()
        order.updated_at = datetime.utcnow()
        updated = await self._sales.update_sales_order(order_id, order)
        if not updated:
            raise NotFoundError("Sales order not found")
        return updated

    async def check_availability(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> tuple[SalesOrder, dict]:
        order = await self.get_sales_order(user, company_id, order_id)
        if order.status not in (
            SalesOrderStatus.CONFIRMED,
            SalesOrderStatus.AVAILABILITY_CHECKED,
        ):
            raise ValidationError(
                "Availability check requires a confirmed or availability_checked sales order"
            )

        summary_lines: list[dict] = []
        all_available = True
        for line in order.lines:
            item = await self._inventory.get_item(line.item_id, company_id)
            on_hand = Decimal("0.0000")
            reserved = Decimal("0.0000")
            if item and item.track_inventory:
                warehouse_id = order.warehouse_id or item.warehouse_id
                balance = await self._inventory.get_balance(
                    company_id, line.item_id, warehouse_id
                )
                on_hand = balance.quantity_on_hand if balance else Decimal("0.0000")
                reserved = balance.quantity_reserved if balance else Decimal("0.0000")
                available = (on_hand - reserved).quantize(Decimal("0.0001"))
                is_available = available >= line.quantity
            else:
                available = line.quantity
                is_available = True

            line.available_quantity = available
            line.is_available = is_available
            if line.id:
                await self._sales.update_so_line_quantities(
                    line.id,
                    available_quantity=available,
                    is_available=is_available,
                )
            if not is_available:
                all_available = False
            summary_lines.append(
                {
                    "sales_order_line_id": line.id,
                    "item_id": line.item_id,
                    "item_sku": line.item_sku,
                    "item_name": line.item_name,
                    "quantity_ordered": line.quantity,
                    "quantity_on_hand": on_hand,
                    "quantity_reserved": reserved,
                    "quantity_available": available,
                    "is_available": is_available,
                }
            )

        order.status = SalesOrderStatus.AVAILABILITY_CHECKED
        order.updated_at = datetime.utcnow()
        updated = await self._sales.update_sales_order(order_id, order)
        if not updated:
            raise NotFoundError("Sales order not found")
        return updated, {"all_available": all_available, "lines": summary_lines}

    async def reserve_stock(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> SalesOrder:
        order = await self.get_sales_order(user, company_id, order_id)
        if order.status not in (
            SalesOrderStatus.AVAILABILITY_CHECKED,
            SalesOrderStatus.CONFIRMED,
        ):
            raise ValidationError(
                "Reserve requires confirmed or availability_checked sales order"
            )
        if any(line.reserved_quantity > 0 for line in order.lines):
            raise ValidationError("Stock is already reserved for this sales order")

        for line in order.lines:
            item = await self._inventory.get_item(line.item_id, company_id)
            if not item or not item.track_inventory:
                line.reserved_quantity = line.quantity
                if line.id:
                    await self._sales.update_so_line_quantities(
                        line.id, reserved_quantity=line.quantity
                    )
                continue

            balance = await self._inventory.get_balance(
                company_id, line.item_id, order.warehouse_id or item.warehouse_id
            )
            on_hand = balance.quantity_on_hand if balance else Decimal("0.0000")
            reserved = balance.quantity_reserved if balance else Decimal("0.0000")
            available = on_hand - reserved
            if available < line.quantity:
                raise ValidationError(
                    f"Insufficient available stock for item '{line.item_sku or line.item_id}': "
                    f"need {line.quantity}, available {available}"
                )
            new_reserved = (reserved + line.quantity).quantize(Decimal("0.0001"))
            await self._inventory.upsert_balance(
                InventoryBalance(
                    id=balance.id if balance else None,
                    company_id=company_id,
                    item_id=line.item_id,
                    warehouse_id=order.warehouse_id or item.warehouse_id,
                    quantity_on_hand=on_hand,
                    quantity_reserved=new_reserved,
                    average_cost=balance.average_cost if balance else Decimal("0.0000"),
                    last_cost=balance.last_cost if balance else Decimal("0.0000"),
                )
            )
            line.reserved_quantity = line.quantity
            if line.id:
                await self._sales.update_so_line_quantities(
                    line.id, reserved_quantity=line.quantity
                )

        order.status = SalesOrderStatus.RESERVED
        order.updated_at = datetime.utcnow()
        updated = await self._sales.update_sales_order(order_id, order)
        if not updated:
            raise NotFoundError("Sales order not found")
        return updated

    async def pick_items(
        self, user: UserRegistration, company_id: str, order_id: str, data: dict | None = None
    ) -> SalesOrder:
        order = await self.get_sales_order(user, company_id, order_id)
        if order.status != SalesOrderStatus.RESERVED:
            raise ValidationError("Pick requires a reserved sales order")

        data = data or {}
        raw_lines = data.get("lines")
        lines_by_id = {line.id: line for line in order.lines if line.id}

        if raw_lines:
            for raw in raw_lines:
                so_line = lines_by_id.get(raw["sales_order_line_id"])
                if not so_line:
                    raise NotFoundError(
                        f"Sales order line '{raw['sales_order_line_id']}' not found"
                    )
                qty = Decimal(str(raw["quantity"]))
                if qty <= 0:
                    raise ValidationError("Pick quantity must be greater than zero")
                max_pick = so_line.reserved_quantity or so_line.quantity
                if qty > max_pick:
                    raise ValidationError(
                        f"Pick qty {qty} exceeds reserved {max_pick} for line {so_line.line_number}"
                    )
                so_line.picked_quantity = qty
                if so_line.id:
                    await self._sales.update_so_line_quantities(
                        so_line.id, picked_quantity=qty
                    )
        else:
            for so_line in order.lines:
                qty = so_line.reserved_quantity or so_line.quantity
                so_line.picked_quantity = qty
                if so_line.id:
                    await self._sales.update_so_line_quantities(
                        so_line.id, picked_quantity=qty
                    )

        order.status = SalesOrderStatus.PICKED
        order.updated_at = datetime.utcnow()
        updated = await self._sales.update_sales_order(order_id, order)
        if not updated:
            raise NotFoundError("Sales order not found")
        return updated

    async def cancel_sales_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> SalesOrder:
        order = await self.get_sales_order(user, company_id, order_id)
        if order.status in (
            SalesOrderStatus.SHIPPED,
            SalesOrderStatus.PARTIALLY_SHIPPED,
            SalesOrderStatus.INVOICED,
            SalesOrderStatus.PARTIALLY_PAID,
            SalesOrderStatus.PAID,
            SalesOrderStatus.CLOSED,
            SalesOrderStatus.CANCELLED,
        ):
            raise ValidationError(f"Cannot cancel sales order in status '{order.status.value}'")

        await self._release_reservations(company_id, order)
        order.status = SalesOrderStatus.CANCELLED
        order.cancelled_at = datetime.utcnow()
        order.updated_at = datetime.utcnow()
        updated = await self._sales.update_sales_order(order_id, order)
        if not updated:
            raise NotFoundError("Sales order not found")
        return updated

    async def close_sales_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> SalesOrder:
        order = await self.get_sales_order(user, company_id, order_id)
        if order.status == SalesOrderStatus.CANCELLED:
            raise ValidationError("Cancelled sales orders cannot be closed")
        if order.status != SalesOrderStatus.PAID:
            raise ValidationError("Sales order can be closed only after full payment")
        order.status = SalesOrderStatus.CLOSED
        order.updated_at = datetime.utcnow()
        updated = await self._sales.update_sales_order(order_id, order)
        if not updated:
            raise NotFoundError("Sales order not found")
        return updated

    # ---- Deliveries ----
    async def create_delivery(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> SalesDelivery:
        await self._get_user_company(user, company_id)
        order = await self._sales.get_sales_order(data["sales_order_id"], company_id)
        if not order:
            raise NotFoundError("Sales order not found")
        if order.status not in (
            SalesOrderStatus.PICKED,
            SalesOrderStatus.PARTIALLY_SHIPPED,
            SalesOrderStatus.RESERVED,
        ):
            raise ValidationError(
                "Delivery requires a picked, reserved, or partially shipped sales order"
            )

        so_lines_by_id = {line.id: line for line in order.lines if line.id}
        raw_lines = data.get("lines") or []
        if not raw_lines:
            raise ValidationError("Delivery must have at least one line")

        lines: list[SalesDeliveryLine] = []
        for index, raw in enumerate(raw_lines, start=1):
            so_line = so_lines_by_id.get(raw["sales_order_line_id"])
            if not so_line:
                raise NotFoundError(
                    f"Sales order line '{raw['sales_order_line_id']}' not found on SO"
                )
            qty = Decimal(str(raw["quantity"]))
            if qty <= 0:
                raise ValidationError("Delivery quantity must be greater than zero")
            remaining = (so_line.picked_quantity or so_line.quantity) - so_line.shipped_quantity
            if qty > remaining:
                raise ValidationError(
                    f"Delivery qty {qty} exceeds remaining {remaining} for line {so_line.line_number}"
                )
            lines.append(
                SalesDeliveryLine(
                    sales_order_line_id=so_line.id or raw["sales_order_line_id"],
                    line_number=index,
                    item_id=so_line.item_id,
                    quantity=qty,
                    notes=raw.get("notes"),
                )
            )

        delivery_number = await self._sales.get_next_delivery_number(company_id)
        delivery = SalesDelivery(
            company_id=company_id,
            delivery_number=delivery_number,
            sales_order_id=order.id or data["sales_order_id"],
            customer_id=order.customer_id,
            delivery_date=data["delivery_date"],
            status=SalesDeliveryStatus.DRAFT,
            notes=data.get("notes"),
            lines=lines,
        )
        return await self._sales.create_delivery(delivery)

    async def get_delivery(
        self, user: UserRegistration, company_id: str, delivery_id: str
    ) -> SalesDelivery:
        await self._get_user_company(user, company_id)
        delivery = await self._sales.get_delivery(delivery_id, company_id)
        if not delivery:
            raise NotFoundError("Sales delivery not found")
        return delivery

    async def list_deliveries(
        self,
        user: UserRegistration,
        company_id: str,
        status: SalesDeliveryStatus | None = None,
        sales_order_id: str | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[SalesDelivery], int]:
        await self._get_user_company(user, company_id)
        items = await self._sales.list_deliveries(
            company_id, status, sales_order_id, skip, limit
        )
        total = await self._sales.count_deliveries(company_id, status, sales_order_id)
        return items, total

    async def confirm_delivery(
        self, user: UserRegistration, company_id: str, delivery_id: str
    ) -> SalesDelivery:
        delivery = await self.get_delivery(user, company_id, delivery_id)
        if delivery.status != SalesDeliveryStatus.DRAFT:
            raise ValidationError("Only draft deliveries can be confirmed")

        order = await self._sales.get_sales_order(delivery.sales_order_id, company_id)
        if not order:
            raise NotFoundError("Sales order not found")
        so_lines_by_id = {line.id: line for line in order.lines if line.id}

        for line in delivery.lines:
            item = await self._inventory.get_item(line.item_id, company_id)
            unit_cost = Decimal("0.0000")
            if item and item.track_inventory:
                warehouse_id = order.warehouse_id or item.warehouse_id
                balance = await self._inventory.get_balance(
                    company_id, line.item_id, warehouse_id
                )
                unit_cost = balance.average_cost if balance else Decimal("0.0000")
                txn = InventoryTransaction(
                    company_id=company_id,
                    item_id=line.item_id,
                    warehouse_id=warehouse_id,
                    txn_type=InventoryTxnType.SALE,
                    txn_date=delivery.delivery_date,
                    quantity_in=Decimal("0.0000"),
                    quantity_out=line.quantity,
                    unit_cost=unit_cost,
                    reference_type="sales_delivery",
                    reference_id=delivery.id,
                    reference_number=delivery.delivery_number,
                    notes=f"SO {order.so_number} / DLV {delivery.delivery_number}",
                )
                _, updated_balance = await self._inventory.create_transaction(txn)
                # Release reserved qty for shipped amount
                release = min(updated_balance.quantity_reserved, line.quantity)
                if release > 0:
                    await self._inventory.upsert_balance(
                        InventoryBalance(
                            id=updated_balance.id,
                            company_id=company_id,
                            item_id=line.item_id,
                            warehouse_id=warehouse_id,
                            quantity_on_hand=updated_balance.quantity_on_hand,
                            quantity_reserved=(
                                updated_balance.quantity_reserved - release
                            ).quantize(Decimal("0.0001")),
                            average_cost=updated_balance.average_cost,
                            last_cost=updated_balance.last_cost,
                        )
                    )

            so_line = so_lines_by_id.get(line.sales_order_line_id)
            if so_line and so_line.id:
                new_shipped = so_line.shipped_quantity + line.quantity
                # reduce reserved on SO line
                new_reserved = max(
                    Decimal("0.0000"), so_line.reserved_quantity - line.quantity
                )
                await self._sales.update_so_line_quantities(
                    so_line.id,
                    shipped_quantity=new_shipped,
                    reserved_quantity=new_reserved,
                )
                so_line.shipped_quantity = new_shipped
                so_line.reserved_quantity = new_reserved

        # Stock-out accounting: Dr COGS, Cr Inventory (updates financial reports).
        await self._post_delivery_cogs_entry(user, company_id, delivery)

        order = await self._sales.get_sales_order(delivery.sales_order_id, company_id)
        if order:
            all_shipped = all(
                line.shipped_quantity >= (line.picked_quantity or line.quantity)
                for line in order.lines
            )
            any_shipped = any(line.shipped_quantity > 0 for line in order.lines)
            if all_shipped:
                order.status = SalesOrderStatus.SHIPPED
            elif any_shipped:
                order.status = SalesOrderStatus.PARTIALLY_SHIPPED
            order.updated_at = datetime.utcnow()
            await self._sales.update_sales_order(order.id or delivery.sales_order_id, order)

        delivery.status = SalesDeliveryStatus.CONFIRMED
        delivery.confirmed_at = datetime.utcnow()
        delivery.updated_at = datetime.utcnow()
        updated = await self._sales.update_delivery(delivery_id, delivery)
        if not updated:
            raise NotFoundError("Sales delivery not found")
        return updated

    # ---- Invoices ----
    async def create_invoice(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> SalesInvoice:
        await self._get_user_company(user, company_id)
        customer = await self._sales.get_customer(data["customer_id"], company_id)
        if not customer:
            raise NotFoundError("Customer not found")

        if data.get("sales_order_id"):
            order = await self._sales.get_sales_order(data["sales_order_id"], company_id)
            if not order:
                raise NotFoundError("Sales order not found")
        if data.get("delivery_id"):
            delivery = await self._sales.get_delivery(data["delivery_id"], company_id)
            if not delivery:
                raise NotFoundError("Sales delivery not found")

        lines = self._build_invoice_lines(data.get("lines") or [])
        subtotal, tax_amount, total_amount = self._sum_invoice_totals(lines)
        invoice_number = await self._sales.get_next_invoice_number(company_id)

        ar_account_id = data.get("ar_account_id") or customer.account_id
        if not await self._is_postable_account(company_id, ar_account_id):
            ar_account_id = await self._ensure_customer_ar_account(company_id, customer)

        invoice = SalesInvoice(
            company_id=company_id,
            invoice_number=invoice_number,
            customer_id=data["customer_id"],
            sales_order_id=data.get("sales_order_id"),
            delivery_id=data.get("delivery_id"),
            invoice_date=data["invoice_date"],
            due_date=data.get("due_date"),
            status=SalesInvoiceStatus.DRAFT,
            ar_account_id=ar_account_id,
            notes=data.get("notes"),
            subtotal=subtotal,
            tax_amount=tax_amount,
            total_amount=total_amount,
            lines=lines,
        )
        return await self._sales.create_invoice(invoice)

    async def get_invoice(
        self, user: UserRegistration, company_id: str, invoice_id: str
    ) -> SalesInvoice:
        await self._get_user_company(user, company_id)
        invoice = await self._sales.get_invoice(invoice_id, company_id)
        if not invoice:
            raise NotFoundError("Sales invoice not found")
        return invoice

    async def list_invoices(
        self,
        user: UserRegistration,
        company_id: str,
        status: SalesInvoiceStatus | None = None,
        customer_id: str | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[SalesInvoice], int]:
        await self._get_user_company(user, company_id)
        items = await self._sales.list_invoices(
            company_id, status, customer_id, skip, limit
        )
        total = await self._sales.count_invoices(company_id, status, customer_id)
        return items, total

    async def post_invoice(
        self, user: UserRegistration, company_id: str, invoice_id: str
    ) -> SalesInvoice:
        if not self._vouchers:
            raise ValidationError("Voucher service is required to post invoices")
        invoice = await self.get_invoice(user, company_id, invoice_id)
        if invoice.status != SalesInvoiceStatus.DRAFT:
            raise ValidationError("Only draft sales invoices can be posted")
        if invoice.total_amount <= 0:
            raise ValidationError("Invoice total must be greater than zero")

        ar_account_id = await self._resolve_ar_account_id(
            company_id=company_id,
            customer_id=invoice.customer_id,
            preferred_ar_account_id=invoice.ar_account_id,
        )
        if not ar_account_id:
            raise ValidationError("AR account is required to post the invoice")

        revenue_account_id = await self._resolve_revenue_account_id(company_id, invoice)
        if not revenue_account_id:
            raise ValidationError(
                "Sales revenue account (4100) is required to post the invoice"
            )

        voucher = await self._vouchers.create_voucher(
            user,
            company_id,
            {
                "voucher_type": VoucherType.SALES.value,
                "voucher_date": invoice.invoice_date,
                "reference": invoice.invoice_number,
                "narration": invoice.notes or f"Sales invoice {invoice.invoice_number}",
                "entries": [
                    {
                        "account_id": ar_account_id,
                        "description": f"AR for invoice {invoice.invoice_number}",
                        "debit_amount": invoice.total_amount,
                        "credit_amount": Decimal("0.00"),
                    },
                    {
                        "account_id": revenue_account_id,
                        "description": f"Sales for invoice {invoice.invoice_number}",
                        "debit_amount": Decimal("0.00"),
                        "credit_amount": invoice.total_amount,
                    },
                ],
            },
        )
        try:
            voucher = await self._vouchers.post_voucher(user, company_id, voucher.id or "")
        except Exception:
            if voucher.id:
                await self._vouchers.cancel_voucher(user, company_id, voucher.id)
            raise

        for line in invoice.lines:
            if line.sales_order_line_id:
                so_line_row = None
                if invoice.sales_order_id:
                    order = await self._sales.get_sales_order(
                        invoice.sales_order_id, company_id
                    )
                    if order:
                        so_line_row = next(
                            (l for l in order.lines if l.id == line.sales_order_line_id),
                            None,
                        )
                if so_line_row and so_line_row.id:
                    new_invoiced = so_line_row.invoiced_quantity + line.quantity
                    await self._sales.update_so_line_quantities(
                        so_line_row.id, invoiced_quantity=new_invoiced
                    )

        invoice.status = SalesInvoiceStatus.POSTED
        invoice.voucher_id = voucher.id
        invoice.ar_account_id = ar_account_id
        invoice.posted_at = datetime.utcnow()
        invoice.updated_at = datetime.utcnow()
        updated = await self._sales.update_invoice(invoice_id, invoice)
        if not updated:
            raise NotFoundError("Sales invoice not found")
        if invoice.sales_order_id:
            await self._refresh_sales_order_workflow_status(
                company_id, invoice.sales_order_id
            )
        return updated

    # ---- Payments ----
    async def create_payment(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> SalesPayment:
        await self._get_user_company(user, company_id)
        customer = await self._sales.get_customer(data["customer_id"], company_id)
        if not customer:
            raise NotFoundError("Customer not found")

        allocations: list[SalesPaymentAllocation] = []
        total = Decimal("0.00")
        for raw in data.get("allocations") or []:
            invoice = await self._sales.get_invoice(raw["sales_invoice_id"], company_id)
            if not invoice:
                raise NotFoundError(f"Sales invoice '{raw['sales_invoice_id']}' not found")
            if invoice.customer_id != data["customer_id"]:
                raise ValidationError("Invoice customer does not match payment customer")
            if invoice.status not in (
                SalesInvoiceStatus.POSTED,
                SalesInvoiceStatus.PARTIALLY_PAID,
            ):
                raise ValidationError(
                    f"Invoice '{invoice.invoice_number}' must be posted or partially paid to allocate"
                )
            amount = Decimal(str(raw["amount"]))
            if amount <= 0:
                raise ValidationError("Allocation amount must be greater than zero")
            outstanding = invoice.total_amount - invoice.amount_paid
            if amount > outstanding:
                raise ValidationError(
                    f"Allocation {amount} exceeds outstanding {outstanding} on invoice {invoice.invoice_number}"
                )
            allocations.append(
                SalesPaymentAllocation(
                    sales_invoice_id=invoice.id or raw["sales_invoice_id"], amount=amount
                )
            )
            total += amount

        if not allocations:
            raise ValidationError("Payment must have at least one allocation")

        payment_number = await self._sales.get_next_payment_number(company_id)
        payment_method = data.get("payment_method", "bank")
        resolved_ar_account_id = await self._resolve_ar_account_id(
            company_id=company_id,
            customer_id=data["customer_id"],
            preferred_ar_account_id=data.get("ar_account_id") or customer.account_id,
        )
        resolved_bank_account_id = await self._resolve_bank_account_id(
            company_id=company_id,
            preferred_bank_account_id=data.get("bank_account_id"),
            payment_method=payment_method,
        )

        payment = SalesPayment(
            company_id=company_id,
            payment_number=payment_number,
            customer_id=data["customer_id"],
            payment_date=data["payment_date"],
            status=SalesPaymentStatus.DRAFT,
            payment_method=payment_method,
            bank_account_id=resolved_bank_account_id,
            ar_account_id=resolved_ar_account_id,
            reference=data.get("reference"),
            notes=data.get("notes"),
            total_amount=total,
            allocations=allocations,
        )
        return await self._sales.create_payment(payment)

    async def get_payment(
        self, user: UserRegistration, company_id: str, payment_id: str
    ) -> SalesPayment:
        await self._get_user_company(user, company_id)
        payment = await self._sales.get_payment(payment_id, company_id)
        if not payment:
            raise NotFoundError("Sales payment not found")
        return payment

    async def list_payments(
        self,
        user: UserRegistration,
        company_id: str,
        status: SalesPaymentStatus | None = None,
        customer_id: str | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[SalesPayment], int]:
        await self._get_user_company(user, company_id)
        items = await self._sales.list_payments(
            company_id, status, customer_id, skip, limit
        )
        total = await self._sales.count_payments(company_id, status, customer_id)
        return items, total

    async def post_payment(
        self, user: UserRegistration, company_id: str, payment_id: str
    ) -> SalesPayment:
        if not self._vouchers:
            raise ValidationError("Voucher service is required to post payments")
        payment = await self.get_payment(user, company_id, payment_id)
        if payment.status != SalesPaymentStatus.DRAFT:
            raise ValidationError("Only draft payments can be posted")
        if payment.total_amount <= 0:
            raise ValidationError("Payment total must be greater than zero")

        ar_account_id = await self._resolve_ar_account_id(
            company_id=company_id,
            customer_id=payment.customer_id,
            preferred_ar_account_id=payment.ar_account_id,
        )
        bank_account_id = await self._resolve_bank_account_id(
            company_id=company_id,
            preferred_bank_account_id=payment.bank_account_id,
            payment_method=payment.payment_method,
        )
        if not ar_account_id:
            raise ValidationError("ar_account_id is required to post payment")
        if not bank_account_id:
            raise ValidationError("bank_account_id is required to post payment")

        voucher = await self._vouchers.create_voucher(
            user,
            company_id,
            {
                "voucher_type": VoucherType.RECEIPT.value,
                "voucher_date": payment.payment_date,
                "reference": payment.payment_number,
                "narration": payment.notes or f"Customer receipt {payment.payment_number}",
                "entries": [
                    {
                        "account_id": bank_account_id,
                        "description": f"Bank/Cash for {payment.payment_number}",
                        "debit_amount": payment.total_amount,
                        "credit_amount": Decimal("0.00"),
                    },
                    {
                        "account_id": ar_account_id,
                        "description": f"AR receipt {payment.payment_number}",
                        "debit_amount": Decimal("0.00"),
                        "credit_amount": payment.total_amount,
                    },
                ],
            },
        )
        try:
            voucher = await self._vouchers.post_voucher(user, company_id, voucher.id or "")
        except Exception:
            if voucher.id:
                await self._vouchers.cancel_voucher(user, company_id, voucher.id)
            raise

        for alloc in payment.allocations:
            invoice = await self._sales.get_invoice(alloc.sales_invoice_id, company_id)
            if not invoice:
                continue
            invoice.amount_paid = (invoice.amount_paid + alloc.amount).quantize(Decimal("0.01"))
            if invoice.amount_paid >= invoice.total_amount:
                invoice.status = SalesInvoiceStatus.PAID
            else:
                invoice.status = SalesInvoiceStatus.PARTIALLY_PAID
            invoice.updated_at = datetime.utcnow()
            await self._sales.update_invoice(invoice.id or alloc.sales_invoice_id, invoice)

        payment.status = SalesPaymentStatus.POSTED
        payment.ar_account_id = ar_account_id
        payment.bank_account_id = bank_account_id
        payment.voucher_id = voucher.id
        payment.posted_at = datetime.utcnow()
        payment.updated_at = datetime.utcnow()
        updated = await self._sales.update_payment(payment_id, payment)
        if not updated:
            raise NotFoundError("Sales payment not found")

        so_ids = set()
        for alloc in payment.allocations:
            inv = await self._sales.get_invoice(alloc.sales_invoice_id, company_id)
            if inv and inv.sales_order_id:
                so_ids.add(inv.sales_order_id)
        for so_id in so_ids:
            await self._refresh_sales_order_workflow_status(company_id, so_id)
        return updated

    # ---- helpers ----
    async def _post_delivery_cogs_entry(
        self, user: UserRegistration, company_id: str, delivery: SalesDelivery
    ) -> None:
        """Stock-out accounting: Dr COGS (or expense), Cr Inventory — auto-posted."""
        if not self._vouchers:
            return

        debit_by_account: dict[str, dict] = {}
        credit_by_account: dict[str, dict] = {}
        total = Decimal("0.00")
        missing_accounts: list[str] = []

        order = await self._sales.get_sales_order(delivery.sales_order_id, company_id)
        warehouse_id = order.warehouse_id if order else None

        for line in delivery.lines:
            item = await self._inventory.get_item(line.item_id, company_id)
            if not item or not item.track_inventory:
                continue
            inventory_account_id = item.inventory_account_id
            cogs_account_id = item.cogs_account_id or item.expense_account_id
            if not inventory_account_id or not cogs_account_id:
                missing_accounts.append(item.sku or item.code or item.name or line.item_id)
                continue
            balance = await self._inventory.get_balance(
                company_id, line.item_id, warehouse_id or item.warehouse_id
            )
            unit_cost = balance.average_cost if balance else Decimal("0.0000")
            amount = (line.quantity * unit_cost).quantize(Decimal("0.01"))
            if amount <= 0:
                continue
            total += amount

            if cogs_account_id not in debit_by_account:
                debit_by_account[cogs_account_id] = {
                    "account_id": cogs_account_id,
                    "description": f"COGS for delivery {delivery.delivery_number}",
                    "debit_amount": Decimal("0.00"),
                    "credit_amount": Decimal("0.00"),
                }
            debit_by_account[cogs_account_id]["debit_amount"] += amount

            if inventory_account_id not in credit_by_account:
                credit_by_account[inventory_account_id] = {
                    "account_id": inventory_account_id,
                    "description": f"Stock out {delivery.delivery_number}",
                    "debit_amount": Decimal("0.00"),
                    "credit_amount": Decimal("0.00"),
                }
            credit_by_account[inventory_account_id]["credit_amount"] += amount

        if missing_accounts:
            raise ValidationError(
                "Cannot post delivery COGS: inventory/COGS account is missing for item(s): "
                + ", ".join(missing_accounts[:8])
                + ("" if len(missing_accounts) <= 8 else "…")
            )

        if total <= 0:
            return

        entries = list(debit_by_account.values()) + list(credit_by_account.values())
        voucher = await self._vouchers.create_voucher(
            user,
            company_id,
            {
                "voucher_type": VoucherType.SALES.value,
                "voucher_date": delivery.delivery_date,
                "reference": delivery.delivery_number,
                "narration": delivery.notes
                or f"Stock out / COGS against {delivery.delivery_number}",
                "entries": entries,
            },
        )
        try:
            await self._vouchers.post_voucher(user, company_id, voucher.id or "")
        except Exception:
            if voucher.id:
                await self._vouchers.cancel_voucher(user, company_id, voucher.id)
            raise

    async def _build_so_lines(
        self, company_id: str, raw_lines: list[dict]
    ) -> list[SalesOrderLine]:
        if not raw_lines:
            raise ValidationError("Sales order must have at least one line")
        lines: list[SalesOrderLine] = []
        for index, raw in enumerate(raw_lines, start=1):
            item = await self._inventory.get_item(raw["item_id"], company_id)
            if not item:
                raise NotFoundError(f"Item '{raw['item_id']}' not found")
            qty = Decimal(str(raw["quantity"]))
            unit_price = Decimal(str(raw["unit_price"]))
            if qty <= 0:
                raise ValidationError("Line quantity must be greater than zero")
            if unit_price < 0:
                raise ValidationError("Unit price cannot be negative")
            tax_rate = Decimal(str(raw.get("tax_rate", 0)))
            base = (qty * unit_price).quantize(Decimal("0.01"))
            tax_amount = (base * tax_rate / Decimal("100")).quantize(Decimal("0.01"))
            lines.append(
                SalesOrderLine(
                    line_number=index,
                    item_id=raw["item_id"],
                    item_sku=item.sku,
                    item_name=item.name,
                    description=raw.get("description") or item.name,
                    quantity=qty,
                    unit_price=unit_price,
                    tax_rate=tax_rate,
                    tax_amount=tax_amount,
                    line_total=(base + tax_amount).quantize(Decimal("0.01")),
                )
            )
        return lines

    def _sum_so_totals(
        self, lines: list[SalesOrderLine]
    ) -> tuple[Decimal, Decimal, Decimal]:
        subtotal = Decimal("0.00")
        tax_amount = Decimal("0.00")
        for line in lines:
            base = (line.quantity * line.unit_price).quantize(Decimal("0.01"))
            subtotal += base
            tax_amount += line.tax_amount
        return subtotal, tax_amount, (subtotal + tax_amount).quantize(Decimal("0.01"))

    def _build_invoice_lines(self, raw_lines: list[dict]) -> list[SalesInvoiceLine]:
        if not raw_lines:
            raise ValidationError("Sales invoice must have at least one line")
        lines: list[SalesInvoiceLine] = []
        for index, raw in enumerate(raw_lines, start=1):
            qty = Decimal(str(raw.get("quantity", 1)))
            unit_price = Decimal(str(raw["unit_price"]))
            tax_rate = Decimal(str(raw.get("tax_rate", 0)))
            if "line_total" in raw and raw["line_total"] is not None:
                line_total = Decimal(str(raw["line_total"])).quantize(Decimal("0.01"))
            else:
                base = (qty * unit_price).quantize(Decimal("0.01"))
                tax = (base * tax_rate / Decimal("100")).quantize(Decimal("0.01"))
                line_total = (base + tax).quantize(Decimal("0.01"))
            lines.append(
                SalesInvoiceLine(
                    line_number=index,
                    item_id=raw.get("item_id"),
                    account_id=raw.get("account_id"),
                    description=raw.get("description"),
                    quantity=qty,
                    unit_price=unit_price,
                    tax_rate=tax_rate,
                    line_total=line_total,
                    sales_order_line_id=raw.get("sales_order_line_id"),
                    sales_delivery_line_id=raw.get("sales_delivery_line_id"),
                )
            )
        return lines

    def _sum_invoice_totals(
        self, lines: list[SalesInvoiceLine]
    ) -> tuple[Decimal, Decimal, Decimal]:
        subtotal = Decimal("0.00")
        tax_amount = Decimal("0.00")
        for line in lines:
            base = (line.quantity * line.unit_price).quantize(Decimal("0.01"))
            tax = (base * line.tax_rate / Decimal("100")).quantize(Decimal("0.01"))
            subtotal += base
            tax_amount += tax
        return subtotal, tax_amount, (subtotal + tax_amount).quantize(Decimal("0.01"))

    async def _release_reservations(self, company_id: str, order: SalesOrder) -> None:
        for line in order.lines:
            if line.reserved_quantity <= 0:
                continue
            item = await self._inventory.get_item(line.item_id, company_id)
            if item and item.track_inventory:
                balance = await self._inventory.get_balance(
                    company_id, line.item_id, order.warehouse_id or item.warehouse_id
                )
                if balance:
                    new_reserved = max(
                        Decimal("0.0000"),
                        balance.quantity_reserved - line.reserved_quantity,
                    )
                    await self._inventory.upsert_balance(
                        InventoryBalance(
                            id=balance.id,
                            company_id=company_id,
                            item_id=line.item_id,
                            warehouse_id=order.warehouse_id or item.warehouse_id,
                            quantity_on_hand=balance.quantity_on_hand,
                            quantity_reserved=new_reserved,
                            average_cost=balance.average_cost,
                            last_cost=balance.last_cost,
                        )
                    )
            if line.id:
                await self._sales.update_so_line_quantities(
                    line.id, reserved_quantity=Decimal("0.0000")
                )
            line.reserved_quantity = Decimal("0.0000")

    async def _refresh_sales_order_workflow_status(
        self, company_id: str, order_id: str
    ) -> None:
        order = await self._sales.get_sales_order(order_id, company_id)
        if not order or order.status in (
            SalesOrderStatus.CANCELLED,
            SalesOrderStatus.CLOSED,
        ):
            return

        invoices = await self._sales.list_invoices(company_id, None, None, 0, 1000)
        so_invoices = [i for i in invoices if i.sales_order_id == order_id]
        if not so_invoices:
            return

        any_paid = any(
            i.status in (SalesInvoiceStatus.PARTIALLY_PAID, SalesInvoiceStatus.PAID)
            for i in so_invoices
        )
        all_paid = all(i.status == SalesInvoiceStatus.PAID for i in so_invoices)
        any_posted = any(
            i.status
            in (
                SalesInvoiceStatus.POSTED,
                SalesInvoiceStatus.PARTIALLY_PAID,
                SalesInvoiceStatus.PAID,
            )
            for i in so_invoices
        )

        if all_paid:
            order.status = SalesOrderStatus.PAID
        elif any_paid:
            order.status = SalesOrderStatus.PARTIALLY_PAID
        elif any_posted:
            order.status = SalesOrderStatus.INVOICED
        order.updated_at = datetime.utcnow()
        await self._sales.update_sales_order(order_id, order)

    async def _resolve_revenue_account_id(
        self, company_id: str, invoice: SalesInvoice
    ) -> str | None:
        for line in invoice.lines:
            if await self._is_postable_account(company_id, line.account_id):
                return line.account_id
        for code in ("4100", "4000"):
            account = await self._accounts.get_by_code(company_id, code)
            if account and account.is_active and not account.is_group:
                return account.id
        return None

    async def _is_postable_account(self, company_id: str, account_id: str | None) -> bool:
        if not account_id:
            return False
        account = await self._accounts.get_by_id(account_id, company_id)
        return bool(account and account.is_active and not account.is_group)

    async def _ensure_customer_ar_account(
        self, company_id: str, customer: Customer
    ) -> str | None:
        account_id = await self._resolve_or_create_customer_ar_account(
            company_id=company_id,
            preferred_account_id=customer.account_id,
            customer_code=customer.code,
            customer_name=customer.name,
            opening_balance=customer.opening_balance or Decimal("0"),
        )
        if account_id and account_id != customer.account_id:
            customer.account_id = account_id
            customer.updated_at = datetime.utcnow()
            if customer.id:
                await self._sales.update_customer(customer.id, customer)
        return account_id

    async def _resolve_ar_account_id(
        self,
        *,
        company_id: str,
        customer_id: str,
        preferred_ar_account_id: str | None,
    ) -> str | None:
        if await self._is_valid_customer_ar_account(company_id, preferred_ar_account_id):
            return preferred_ar_account_id

        customer = await self._sales.get_customer(customer_id, company_id)
        if customer:
            ensured = await self._ensure_customer_ar_account(company_id, customer)
            if ensured:
                return ensured

        ar_control = await self._accounts.get_by_code(company_id, "1130")
        if ar_control and not ar_control.is_group and ar_control.is_active:
            return ar_control.id
        return None

    async def _resolve_bank_account_id(
        self,
        *,
        company_id: str,
        preferred_bank_account_id: str | None,
        payment_method: str | None = None,
    ) -> str | None:
        if await self._is_postable_account(company_id, preferred_bank_account_id):
            return preferred_bank_account_id
        method = (payment_method or "bank").lower()
        preferred_codes = ("1110", "1120") if method == "cash" else ("1120", "1110")
        for code in preferred_codes:
            account = await self._accounts.get_by_code(company_id, code)
            if account and not account.is_group and account.is_active:
                return account.id
        return None

    async def _is_valid_customer_ar_account(
        self, company_id: str, account_id: str | None
    ) -> bool:
        if not account_id:
            return False
        account = await self._accounts.get_by_id(account_id, company_id)
        if not account or not account.is_active or account.is_group:
            return False
        return str(account.code or "").strip().isdigit()

    async def _rename_account_to_next_customer_code(
        self, company_id: str, account_id: str
    ) -> str:
        account = await self._accounts.get_by_id(account_id, company_id)
        if not account:
            raise NotFoundError("Customer AR account not found")
        new_code = await self._next_customer_ar_code(company_id)
        account.code = new_code
        account.updated_at = datetime.utcnow()
        updated = await self._accounts.update(account_id, account)
        if not updated:
            raise ValidationError("Failed to repair customer AR account code")
        return account_id

    async def _resolve_or_create_customer_ar_account(
        self,
        *,
        company_id: str,
        preferred_account_id: str | None,
        customer_code: str,
        customer_name: str,
        opening_balance: Decimal = Decimal("0"),
    ) -> str:
        if await self._is_valid_customer_ar_account(company_id, preferred_account_id):
            return preferred_account_id or ""

        if preferred_account_id:
            existing = await self._accounts.get_by_id(preferred_account_id, company_id)
            if existing and not existing.is_group:
                code = str(existing.code or "").strip().upper()
                if code.startswith("AR-CUS") or not str(existing.code or "").isdigit():
                    return await self._rename_account_to_next_customer_code(
                        company_id, preferred_account_id
                    )

        return await self._create_customer_ar_account(
            company_id=company_id,
            customer_code=customer_code,
            customer_name=customer_name,
            opening_balance=opening_balance,
        )

    async def _resolve_customer_ar_parent(self, company_id: str) -> ChartOfAccount | None:
        for code in ("1140", "1130", "1100"):
            parent = await self._accounts.get_by_code(company_id, code)
            if parent and parent.is_group and parent.is_active:
                return parent
        return None

    async def _next_customer_ar_code(self, company_id: str) -> str:
        parent = await self._resolve_customer_ar_parent(company_id)
        if not parent or not parent.id:
            raise ValidationError(
                "Customers group account (1140) is required before creating customer AR accounts"
            )
        if not str(parent.code or "").isdigit():
            raise ValidationError(
                f"Customer AR parent account code '{parent.code}' must be numeric"
            )

        accounts = await self._accounts.list_all(
            company_id, AccountType.ASSET, None, 0, 5000
        )
        sibling_nums: list[int] = []
        parent_code = str(parent.code)
        for account in accounts:
            code = str(account.code or "").strip()
            if not code.isdigit():
                continue
            if account.parent_id == parent.id or (
                code.startswith(parent_code) and len(code) > len(parent_code)
            ):
                sibling_nums.append(int(code))

        next_num = (max(sibling_nums) + 1) if sibling_nums else (int(parent_code) + 1)
        for _ in range(10000):
            candidate = str(next_num)
            if not await self._accounts.get_by_code(company_id, candidate):
                return candidate
            next_num += 1
        raise ValidationError("Unable to allocate AR account code for customer")

    async def _create_customer_ar_account(
        self,
        *,
        company_id: str,
        customer_code: str,
        customer_name: str,
        opening_balance: Decimal = Decimal("0"),
    ) -> str:
        parent = await self._resolve_customer_ar_parent(company_id)
        if not parent:
            raise ValidationError(
                "Customers group account (1140) is required before creating customer AR accounts"
            )
        code = await self._next_customer_ar_code(company_id)
        account = ChartOfAccount(
            company_id=company_id,
            code=code,
            name=f"{customer_code} - {customer_name}"[:200],
            account_type=AccountType.ASSET,
            nature=account_nature_for_type(AccountType.ASSET),
            parent_id=parent.id,
            level=parent.level + 1,
            is_group=False,
            is_active=True,
            opening_balance=opening_balance,
            current_balance=opening_balance,
            description=f"Accounts receivable for customer {customer_code}",
        )
        created = await self._accounts.create(account)
        if not created.id:
            raise ValidationError("Failed to create AR account for customer")
        return created.id

    async def _get_user_company(self, user: UserRegistration, company_id: str) -> Company:
        return await resolve_company_for_user(self._companies, user, company_id)
