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 InventoryTransaction
from app.domain.entities.purchase import (
    GoodsReceipt,
    GoodsReceiptLine,
    PurchaseOrder,
    PurchaseOrderLine,
    PurchasePayment,
    PurchasePaymentAllocation,
    Vendor,
    VendorBill,
    VendorBillLine,
)
from app.domain.entities.user_registration import UserRegistration
from app.domain.enums import (
    AccountType,
    AmountType,
    GrnStatus,
    InventoryTxnType,
    PaymentTerms,
    PreferredPaymentMethod,
    PurchaseOrderStatus,
    PurchasePaymentStatus,
    PurchaseType,
    VendorBillStatus,
    VendorBusinessType,
    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.purchase_repository import PurchaseRepository


class PurchaseService:
    def __init__(
        self,
        purchase_repository: PurchaseRepository,
        inventory_repository: InventoryRepository,
        account_repository: ChartOfAccountRepository,
        company_repository: CompanyRepository,
        voucher_service: VoucherService,
    ) -> None:
        self._purchases = purchase_repository
        self._inventory = inventory_repository
        self._accounts = account_repository
        self._companies = company_repository
        self._vouchers = voucher_service

    # ---- Vendors ----
    _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,
    }

    async def create_vendor(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> Vendor:
        await self._get_user_company(user, company_id)
        code = (data.get("code") or "").strip() or await self._purchases.get_next_vendor_code(
            company_id
        )
        if await self._purchases.get_vendor_by_code(company_id, code):
            raise ConflictError(f"Vendor 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 = VendorBusinessType(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_vendor_ap_account(
            company_id=company_id,
            preferred_account_id=data.get("account_id") or None,
            vendor_code=code,
            vendor_name=data["name"],
            opening_balance=opening_balance,
        )

        vendor = Vendor(
            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",
            po_prefix=data.get("po_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._purchases.create_vendor(vendor)

    async def get_vendor(
        self, user: UserRegistration, company_id: str, vendor_id: str
    ) -> Vendor:
        await self._get_user_company(user, company_id)
        vendor = await self._purchases.get_vendor(vendor_id, company_id)
        if not vendor:
            raise NotFoundError("Vendor not found")
        return vendor

    async def list_vendors(
        self,
        user: UserRegistration,
        company_id: str,
        is_active: bool | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[Vendor], int]:
        await self._get_user_company(user, company_id)
        items = await self._purchases.list_vendors(company_id, is_active, skip, limit)
        total = await self._purchases.count_vendors(company_id, is_active)
        return items, total

    async def update_vendor(
        self, user: UserRegistration, company_id: str, vendor_id: str, data: dict
    ) -> Vendor:
        vendor = await self.get_vendor(user, company_id, vendor_id)
        if "code" in data and data["code"] and data["code"] != vendor.code:
            if await self._purchases.get_vendor_by_code(company_id, data["code"]):
                raise ConflictError(f"Vendor code '{data['code']}' already exists")
            vendor.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")
            vendor.default_warehouse_id = warehouse_id
        if "business_type" in data:
            value = data["business_type"]
            vendor.business_type = VendorBusinessType(value) if value else None
        if "payment_terms" in data:
            value = data["payment_terms"]
            vendor.payment_terms = PaymentTerms(value) if value else None
            if vendor.payment_terms and "payment_terms_days" not in data:
                vendor.payment_terms_days = self._PAYMENT_TERMS_DAYS.get(
                    vendor.payment_terms, vendor.payment_terms_days
                )
        if "preferred_payment_method" in data:
            value = data["preferred_payment_method"]
            vendor.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",
            "po_prefix",
            "logo",
            "account_id",
            "is_active",
        ):
            if field in data:
                setattr(vendor, field, data[field])
        if "credit_limit" in data:
            vendor.credit_limit = Decimal(str(data["credit_limit"]))
        if "opening_balance" in data:
            vendor.opening_balance = Decimal(str(data["opening_balance"]))
        if "address_line1" in data:
            vendor.address = data["address_line1"]

        # Auto-create / repair AP account when missing or still using AP-VEN-* codes.
        vendor.account_id = await self._resolve_or_create_vendor_ap_account(
            company_id=company_id,
            preferred_account_id=vendor.account_id,
            vendor_code=vendor.code,
            vendor_name=vendor.name,
            opening_balance=vendor.opening_balance or Decimal("0"),
        )

        vendor.updated_at = datetime.utcnow()
        updated = await self._purchases.update_vendor(vendor_id, vendor)
        if not updated:
            raise NotFoundError("Vendor not found")
        return updated

    async def delete_vendor(
        self, user: UserRegistration, company_id: str, vendor_id: str
    ) -> None:
        await self.get_vendor(user, company_id, vendor_id)
        if not await self._purchases.delete_vendor(vendor_id):
            raise NotFoundError("Vendor not found")

    # ---- Purchase orders ----
    async def create_purchase_order(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> PurchaseOrder:
        await self._get_user_company(user, company_id)
        vendor = await self._purchases.get_vendor(data["vendor_id"], company_id)
        if not vendor:
            raise NotFoundError("Vendor not found")

        warehouse_id = data.get("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_po_lines(company_id, data.get("lines") or [])
        subtotal, discount_amount, tax_amount, total_amount = self._sum_line_totals(lines)
        po_number = data.get("po_number") or await self._purchases.get_next_po_number(company_id)

        payment_terms = data.get("payment_terms") or vendor.payment_terms
        if isinstance(payment_terms, str):
            payment_terms = PaymentTerms(payment_terms)
        purchase_type = data.get("purchase_type")
        if isinstance(purchase_type, str):
            purchase_type = PurchaseType(purchase_type)

        order = PurchaseOrder(
            company_id=company_id,
            po_number=po_number,
            vendor_id=data["vendor_id"],
            order_date=data["order_date"],
            expected_date=data.get("expected_date"),
            delivery_date=data.get("delivery_date"),
            payment_terms=payment_terms,
            currency=data.get("currency") or vendor.currency or "PKR",
            warehouse_id=warehouse_id,
            ship_to=data.get("ship_to"),
            purchase_type=purchase_type,
            reference_number=data.get("reference_number"),
            department=data.get("department"),
            notes=data.get("notes"),
            terms_and_conditions=data.get("terms_and_conditions"),
            attachments=list(data.get("attachments") or []),
            status=PurchaseOrderStatus.PENDING,
            subtotal=subtotal,
            discount_amount=discount_amount,
            tax_amount=tax_amount,
            total_amount=total_amount,
            created_by=user.id,
            lines=lines,
        )
        return await self._purchases.create_purchase_order(order)

    async def get_purchase_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> PurchaseOrder:
        await self._get_user_company(user, company_id)
        order = await self._purchases.get_purchase_order(order_id, company_id)
        if not order:
            raise NotFoundError("Purchase order not found")
        return order

    async def list_purchase_orders(
        self,
        user: UserRegistration,
        company_id: str,
        status: PurchaseOrderStatus | None = None,
        vendor_id: str | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[PurchaseOrder], int]:
        await self._get_user_company(user, company_id)
        items = await self._purchases.list_purchase_orders(
            company_id, status, vendor_id, skip, limit
        )
        total = await self._purchases.count_purchase_orders(company_id, status, vendor_id)
        return items, total

    async def update_purchase_order(
        self, user: UserRegistration, company_id: str, order_id: str, data: dict
    ) -> PurchaseOrder:
        order = await self.get_purchase_order(user, company_id, order_id)
        if order.status != PurchaseOrderStatus.PENDING:
            raise ValidationError("Only pending purchase orders can be updated")

        if "vendor_id" in data:
            vendor = await self._purchases.get_vendor(data["vendor_id"], company_id)
            if not vendor:
                raise NotFoundError("Vendor not found")
            order.vendor_id = data["vendor_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
        if "purchase_type" in data:
            value = data["purchase_type"]
            order.purchase_type = PurchaseType(value) if value else None
        for field in (
            "order_date",
            "expected_date",
            "delivery_date",
            "currency",
            "ship_to",
            "reference_number",
            "department",
            "notes",
            "terms_and_conditions",
        ):
            if field in data:
                setattr(order, field, data[field])
        if "attachments" in data and data["attachments"] is not None:
            order.attachments = list(data["attachments"])

        replace_lines = False
        if "lines" in data:
            order.lines = await self._build_po_lines(company_id, data["lines"])
            subtotal, discount_amount, tax_amount, total_amount = self._sum_line_totals(order.lines)
            order.subtotal = subtotal
            order.discount_amount = discount_amount
            order.tax_amount = tax_amount
            order.total_amount = total_amount
            replace_lines = True

        order.updated_at = datetime.utcnow()
        updated = await self._purchases.update_purchase_order(
            order_id, order, replace_lines=replace_lines
        )
        if not updated:
            raise NotFoundError("Purchase order not found")
        return updated

    async def add_purchase_order_attachments(
        self,
        user: UserRegistration,
        company_id: str,
        order_id: str,
        paths: list[str],
    ) -> PurchaseOrder:
        order = await self.get_purchase_order(user, company_id, order_id)
        if order.status != PurchaseOrderStatus.PENDING:
            raise ValidationError("Only pending purchase orders can be updated")
        order.attachments = list(order.attachments or []) + paths
        order.updated_at = datetime.utcnow()
        updated = await self._purchases.update_purchase_order(order_id, order)
        if not updated:
            raise NotFoundError("Purchase order not found")
        return updated

    async def submit_purchase_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> PurchaseOrder:
        order = await self.get_purchase_order(user, company_id, order_id)
        if order.status != PurchaseOrderStatus.PENDING:
            raise ValidationError("Only pending purchase orders can be submitted")
        if not order.lines:
            raise ValidationError("Purchase order must have at least one line")
        order.status = PurchaseOrderStatus.CONFIRMED
        order.updated_at = datetime.utcnow()
        updated = await self._purchases.update_purchase_order(order_id, order)
        if not updated:
            raise NotFoundError("Purchase order not found")
        return updated

    async def confirm_vendor(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> PurchaseOrder:
        return await self.submit_purchase_order(user, company_id, order_id)

    async def approve_purchase_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> PurchaseOrder:
        order = await self.get_purchase_order(user, company_id, order_id)
        if order.status != PurchaseOrderStatus.CONFIRMED:
            raise ValidationError("Only confirmed purchase orders can be approved")
        order.status = PurchaseOrderStatus.CONFIRMED
        order.approved_at = datetime.utcnow()
        order.updated_at = datetime.utcnow()
        updated = await self._purchases.update_purchase_order(order_id, order)
        if not updated:
            raise NotFoundError("Purchase order not found")
        return updated

    async def cancel_purchase_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> PurchaseOrder:
        order = await self.get_purchase_order(user, company_id, order_id)
        if order.status in (
            PurchaseOrderStatus.RECEIVED,
            PurchaseOrderStatus.CLOSED,
            PurchaseOrderStatus.CANCELLED,
        ):
            raise ValidationError(f"Cannot cancel purchase order in status '{order.status.value}'")
        if order.status in (
            PurchaseOrderStatus.PARTIALLY_RECEIVED,
        ):
            raise ValidationError("Cannot cancel a partially received purchase order")
        order.status = PurchaseOrderStatus.CANCELLED
        order.cancelled_at = datetime.utcnow()
        order.updated_at = datetime.utcnow()
        updated = await self._purchases.update_purchase_order(order_id, order)
        if not updated:
            raise NotFoundError("Purchase order not found")
        return updated

    async def close_purchase_order(
        self, user: UserRegistration, company_id: str, order_id: str
    ) -> PurchaseOrder:
        order = await self.get_purchase_order(user, company_id, order_id)
        if order.status in (PurchaseOrderStatus.CANCELLED,):
            raise ValidationError("Cancelled purchase orders cannot be closed")
        if order.status != PurchaseOrderStatus.PAID:
            raise ValidationError("Purchase order can be closed only after full payment")
        order.status = PurchaseOrderStatus.CLOSED
        order.updated_at = datetime.utcnow()
        updated = await self._purchases.update_purchase_order(order_id, order)
        if not updated:
            raise NotFoundError("Purchase order not found")
        return updated

    # ---- Goods receipts ----
    async def create_goods_receipt(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> GoodsReceipt:
        await self._get_user_company(user, company_id)
        order = await self._purchases.get_purchase_order(data["purchase_order_id"], company_id)
        if not order:
            raise NotFoundError("Purchase order not found")
        if order.status not in (
            PurchaseOrderStatus.CONFIRMED,
            PurchaseOrderStatus.PARTIALLY_RECEIVED,
        ):
            raise ValidationError("Goods receipt requires a confirmed or partially received PO")

        po_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("Goods receipt must have at least one line")

        lines: list[GoodsReceiptLine] = []
        for index, raw in enumerate(raw_lines, start=1):
            po_line = po_lines_by_id.get(raw["purchase_order_line_id"])
            if not po_line:
                raise NotFoundError(
                    f"Purchase order line '{raw['purchase_order_line_id']}' not found on PO"
                )
            qty = Decimal(str(raw["quantity_received"]))
            if qty <= 0:
                raise ValidationError("quantity_received must be greater than zero")
            remaining = po_line.quantity - po_line.received_quantity
            if qty > remaining:
                raise ValidationError(
                    f"Received qty {qty} exceeds remaining {remaining} for line {po_line.line_number}"
                )
            lines.append(
                GoodsReceiptLine(
                    purchase_order_line_id=po_line.id or raw["purchase_order_line_id"],
                    line_number=index,
                    item_id=po_line.item_id,
                    quantity_ordered=po_line.quantity,
                    quantity_received=qty,
                    unit_cost=Decimal(
                        str(
                            raw["unit_cost"]
                            if raw.get("unit_cost") is not None
                            else po_line.unit_price
                        )
                    ),
                    notes=raw.get("notes"),
                )
            )

        grn_number = await self._purchases.get_next_grn_number(company_id)
        receipt = GoodsReceipt(
            company_id=company_id,
            grn_number=grn_number,
            purchase_order_id=order.id or data["purchase_order_id"],
            vendor_id=order.vendor_id,
            receipt_date=data["receipt_date"],
            status=GrnStatus.DRAFT,
            notes=data.get("notes"),
            lines=lines,
        )
        return await self._purchases.create_goods_receipt(receipt)

    async def get_goods_receipt(
        self, user: UserRegistration, company_id: str, receipt_id: str
    ) -> GoodsReceipt:
        await self._get_user_company(user, company_id)
        receipt = await self._purchases.get_goods_receipt(receipt_id, company_id)
        if not receipt:
            raise NotFoundError("Goods receipt not found")
        return receipt

    async def list_goods_receipts(
        self,
        user: UserRegistration,
        company_id: str,
        status: GrnStatus | None = None,
        purchase_order_id: str | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[GoodsReceipt], int]:
        await self._get_user_company(user, company_id)
        items = await self._purchases.list_goods_receipts(
            company_id, status, purchase_order_id, skip, limit
        )
        total = await self._purchases.count_goods_receipts(
            company_id, status, purchase_order_id
        )
        return items, total

    async def confirm_goods_receipt(
        self, user: UserRegistration, company_id: str, receipt_id: str
    ) -> GoodsReceipt:
        receipt = await self.get_goods_receipt(user, company_id, receipt_id)
        if receipt.status != GrnStatus.DRAFT:
            raise ValidationError("Only draft goods receipts can be confirmed")

        order = await self._purchases.get_purchase_order(receipt.purchase_order_id, company_id)
        if not order:
            raise NotFoundError("Purchase order not found")

        po_lines_by_id = {line.id: line for line in order.lines if line.id}

        for line in receipt.lines:
            item = await self._inventory.get_item(line.item_id, company_id)
            if item and item.track_inventory:
                txn = InventoryTransaction(
                    company_id=company_id,
                    item_id=line.item_id,
                    warehouse_id=order.warehouse_id or item.warehouse_id,
                    txn_type=InventoryTxnType.PURCHASE_RECEIPT,
                    txn_date=receipt.receipt_date,
                    quantity_in=line.quantity_received,
                    quantity_out=Decimal("0.0000"),
                    unit_cost=line.unit_cost,
                    reference_type="goods_receipt",
                    reference_id=receipt.id,
                    reference_number=receipt.grn_number,
                    notes=f"GRN {receipt.grn_number}",
                )
                await self._inventory.create_transaction(txn)

            po_line = po_lines_by_id.get(line.purchase_order_line_id)
            if po_line and po_line.id:
                new_received = po_line.received_quantity + line.quantity_received
                await self._purchases.update_po_line_received_quantity(po_line.id, new_received)
                po_line.received_quantity = new_received

        # Financial entry for stock-in:
        # Dr Inventory (item inventory accounts), Cr Accrued Expenses (GRNI placeholder, code 2120).
        await self._post_grn_accounting_entry(user, company_id, receipt)

        # Refresh PO status
        order = await self._purchases.get_purchase_order(receipt.purchase_order_id, company_id)
        if order:
            all_received = all(line.received_quantity >= line.quantity for line in order.lines)
            any_received = any(line.received_quantity > 0 for line in order.lines)
            if all_received:
                order.status = PurchaseOrderStatus.RECEIVED
            elif any_received:
                order.status = PurchaseOrderStatus.PARTIALLY_RECEIVED
            order.updated_at = datetime.utcnow()
            await self._purchases.update_purchase_order(order.id or receipt.purchase_order_id, order)

        receipt.status = GrnStatus.CONFIRMED
        receipt.confirmed_at = datetime.utcnow()
        receipt.updated_at = datetime.utcnow()
        updated = await self._purchases.update_goods_receipt(receipt_id, receipt)
        if not updated:
            raise NotFoundError("Goods receipt not found")
        return updated

    # ---- Vendor bills ----
    async def create_vendor_bill(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> VendorBill:
        await self._get_user_company(user, company_id)
        vendor = await self._purchases.get_vendor(data["vendor_id"], company_id)
        if not vendor:
            raise NotFoundError("Vendor not found")

        if data.get("purchase_order_id"):
            order = await self._purchases.get_purchase_order(data["purchase_order_id"], company_id)
            if not order:
                raise NotFoundError("Purchase order not found")
        if data.get("goods_receipt_id"):
            grn = await self._purchases.get_goods_receipt(data["goods_receipt_id"], company_id)
            if not grn:
                raise NotFoundError("Goods receipt not found")

        lines = self._build_bill_lines(data.get("lines") or [])
        subtotal, tax_amount, total_amount = self._sum_bill_totals(lines)
        bill_number = await self._purchases.get_next_bill_number(company_id)

        ap_account_id = data.get("ap_account_id") or vendor.account_id
        if not await self._is_postable_account(company_id, ap_account_id):
            ap_account_id = await self._ensure_vendor_ap_account(company_id, vendor)

        bill = VendorBill(
            company_id=company_id,
            bill_number=bill_number,
            vendor_invoice_number=data.get("vendor_invoice_number"),
            vendor_id=data["vendor_id"],
            purchase_order_id=data.get("purchase_order_id"),
            goods_receipt_id=data.get("goods_receipt_id"),
            bill_date=data["bill_date"],
            due_date=data.get("due_date"),
            status=VendorBillStatus.DRAFT,
            ap_account_id=ap_account_id,
            notes=data.get("notes"),
            subtotal=subtotal,
            tax_amount=tax_amount,
            total_amount=total_amount,
            lines=lines,
        )
        return await self._purchases.create_vendor_bill(bill)

    async def get_vendor_bill(
        self, user: UserRegistration, company_id: str, bill_id: str
    ) -> VendorBill:
        await self._get_user_company(user, company_id)
        bill = await self._purchases.get_vendor_bill(bill_id, company_id)
        if not bill:
            raise NotFoundError("Vendor bill not found")
        return bill

    async def list_vendor_bills(
        self,
        user: UserRegistration,
        company_id: str,
        status: VendorBillStatus | None = None,
        vendor_id: str | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[VendorBill], int]:
        await self._get_user_company(user, company_id)
        items = await self._purchases.list_vendor_bills(
            company_id, status, vendor_id, skip, limit
        )
        total = await self._purchases.count_vendor_bills(company_id, status, vendor_id)
        return items, total

    async def post_vendor_bill(
        self, user: UserRegistration, company_id: str, bill_id: str
    ) -> VendorBill:
        bill = await self.get_vendor_bill(user, company_id, bill_id)
        if bill.status != VendorBillStatus.DRAFT:
            raise ValidationError("Only draft vendor bills can be posted")
        if bill.total_amount <= 0:
            raise ValidationError("Bill total must be greater than zero")

        ap_account_id = await self._resolve_ap_account_id(
            company_id=company_id,
            vendor_id=bill.vendor_id,
            preferred_ap_account_id=bill.ap_account_id,
        )
        if not ap_account_id:
            raise ValidationError("AP account is required to post the bill")

        # Build debit entries:
        # If bill is against GRN and GRNI account exists, clear accrued (Dr GRNI).
        # Otherwise, post line-wise to inventory/expense accounts.
        debit_entries: list[dict] = []
        grni_account = await self._accounts.get_by_code(company_id, "2120")
        if bill.goods_receipt_id and grni_account and not grni_account.is_group:
            debit_entries.append(
                {
                    "account_id": grni_account.id or "",
                    "description": f"GRN accrual clear for bill {bill.bill_number}",
                    "debit_amount": bill.total_amount,
                    "credit_amount": Decimal("0.00"),
                }
            )
        else:
            for line in bill.lines:
                account_id = line.account_id
                if not account_id and line.item_id:
                    item = await self._inventory.get_item(line.item_id, company_id)
                    if item:
                        account_id = item.inventory_account_id or item.expense_account_id
                if not account_id:
                    raise ValidationError(
                        f"Bill line {line.line_number} has no account_id or item inventory/expense account"
                    )
                debit_entries.append(
                    {
                        "account_id": account_id,
                        "description": line.description or f"Bill {bill.bill_number}",
                        "debit_amount": line.line_total,
                        "credit_amount": Decimal("0.00"),
                    }
                )

        # Aggregate same-account debits for cleaner voucher
        aggregated: dict[str, dict] = {}
        for entry in debit_entries:
            key = entry["account_id"]
            if key not in aggregated:
                aggregated[key] = {
                    "account_id": key,
                    "description": entry["description"],
                    "debit_amount": Decimal("0.00"),
                    "credit_amount": Decimal("0.00"),
                }
            aggregated[key]["debit_amount"] += Decimal(str(entry["debit_amount"]))

        entries = list(aggregated.values())
        entries.append(
            {
                "account_id": ap_account_id,
                "description": f"AP for bill {bill.bill_number}",
                "debit_amount": Decimal("0.00"),
                "credit_amount": bill.total_amount,
            }
        )

        voucher = await self._vouchers.create_voucher(
            user,
            company_id,
            {
                "voucher_type": VoucherType.PURCHASE.value,
                "voucher_date": bill.bill_date,
                "reference": bill.bill_number,
                "narration": bill.notes or f"Vendor bill {bill.bill_number}",
                "entries": entries,
            },
        )
        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

        bill.status = VendorBillStatus.POSTED
        bill.voucher_id = voucher.id
        bill.ap_account_id = ap_account_id
        bill.posted_at = datetime.utcnow()
        bill.updated_at = datetime.utcnow()
        updated = await self._purchases.update_vendor_bill(bill_id, bill)
        if not updated:
            raise NotFoundError("Vendor bill not found")
        if bill.purchase_order_id:
            await self._refresh_purchase_order_workflow_status(company_id, bill.purchase_order_id)
        return updated

    # ---- Payments ----
    async def create_payment(
        self, user: UserRegistration, company_id: str, data: dict
    ) -> PurchasePayment:
        await self._get_user_company(user, company_id)
        vendor = await self._purchases.get_vendor(data["vendor_id"], company_id)
        if not vendor:
            raise NotFoundError("Vendor not found")

        allocations: list[PurchasePaymentAllocation] = []
        total = Decimal("0.00")
        for raw in data.get("allocations") or []:
            bill = await self._purchases.get_vendor_bill(raw["vendor_bill_id"], company_id)
            if not bill:
                raise NotFoundError(f"Vendor bill '{raw['vendor_bill_id']}' not found")
            if bill.vendor_id != data["vendor_id"]:
                raise ValidationError("Bill vendor does not match payment vendor")
            if bill.status not in (
                VendorBillStatus.POSTED,
                VendorBillStatus.PARTIALLY_PAID,
            ):
                raise ValidationError(
                    f"Bill '{bill.bill_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 = bill.total_amount - bill.amount_paid
            if amount > outstanding:
                raise ValidationError(
                    f"Allocation {amount} exceeds outstanding {outstanding} on bill {bill.bill_number}"
                )
            allocations.append(
                PurchasePaymentAllocation(vendor_bill_id=bill.id or raw["vendor_bill_id"], amount=amount)
            )
            total += amount

        if not allocations:
            raise ValidationError("Payment must have at least one allocation")

        payment_number = await self._purchases.get_next_payment_number(company_id)
        payment_method = data.get("payment_method", "bank")
        resolved_ap_account_id = await self._resolve_ap_account_id(
            company_id=company_id,
            vendor_id=data["vendor_id"],
            preferred_ap_account_id=data.get("ap_account_id") or vendor.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 = PurchasePayment(
            company_id=company_id,
            payment_number=payment_number,
            vendor_id=data["vendor_id"],
            payment_date=data["payment_date"],
            status=PurchasePaymentStatus.DRAFT,
            payment_method=payment_method,
            bank_account_id=resolved_bank_account_id,
            ap_account_id=resolved_ap_account_id,
            reference=data.get("reference"),
            notes=data.get("notes"),
            total_amount=total,
            allocations=allocations,
        )
        return await self._purchases.create_payment(payment)

    async def get_payment(
        self, user: UserRegistration, company_id: str, payment_id: str
    ) -> PurchasePayment:
        await self._get_user_company(user, company_id)
        payment = await self._purchases.get_payment(payment_id, company_id)
        if not payment:
            raise NotFoundError("Purchase payment not found")
        return payment

    async def list_payments(
        self,
        user: UserRegistration,
        company_id: str,
        status: PurchasePaymentStatus | None = None,
        vendor_id: str | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[PurchasePayment], int]:
        await self._get_user_company(user, company_id)
        items = await self._purchases.list_payments(company_id, status, vendor_id, skip, limit)
        total = await self._purchases.count_payments(company_id, status, vendor_id)
        return items, total

    async def post_payment(
        self, user: UserRegistration, company_id: str, payment_id: str
    ) -> PurchasePayment:
        payment = await self.get_payment(user, company_id, payment_id)
        if payment.status != PurchasePaymentStatus.DRAFT:
            raise ValidationError("Only draft payments can be posted")
        if payment.total_amount <= 0:
            raise ValidationError("Payment total must be greater than zero")

        ap_account_id = await self._resolve_ap_account_id(
            company_id=company_id,
            vendor_id=payment.vendor_id,
            preferred_ap_account_id=payment.ap_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 ap_account_id:
            raise ValidationError("ap_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.PAYMENT.value,
                "voucher_date": payment.payment_date,
                "reference": payment.payment_number,
                "narration": payment.notes or f"Purchase payment {payment.payment_number}",
                "entries": [
                    {
                        "account_id": ap_account_id,
                        "description": f"AP payment {payment.payment_number}",
                        "debit_amount": payment.total_amount,
                        "credit_amount": Decimal("0.00"),
                    },
                    {
                        "account_id": bank_account_id,
                        "description": f"Bank/Cash for {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:
            # Avoid leaving orphan draft vouchers when post fails.
            if voucher.id:
                await self._vouchers.cancel_voucher(user, company_id, voucher.id)
            raise

        for alloc in payment.allocations:
            bill = await self._purchases.get_vendor_bill(alloc.vendor_bill_id, company_id)
            if not bill:
                continue
            bill.amount_paid = (bill.amount_paid + alloc.amount).quantize(Decimal("0.01"))
            if bill.amount_paid >= bill.total_amount:
                bill.status = VendorBillStatus.PAID
            else:
                bill.status = VendorBillStatus.PARTIALLY_PAID
            bill.updated_at = datetime.utcnow()
            await self._purchases.update_vendor_bill(bill.id or alloc.vendor_bill_id, bill)

        payment.status = PurchasePaymentStatus.POSTED
        payment.ap_account_id = ap_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._purchases.update_payment(payment_id, payment)
        if not updated:
            raise NotFoundError("Purchase payment not found")
        po_ids = set()
        for alloc in payment.allocations:
            b = await self._purchases.get_vendor_bill(alloc.vendor_bill_id, company_id)
            if b and b.purchase_order_id:
                po_ids.add(b.purchase_order_id)
        for po_id in po_ids:
            await self._refresh_purchase_order_workflow_status(company_id, po_id)
        return updated

    # ---- helpers ----
    async def _build_po_lines(
        self, company_id: str, raw_lines: list[dict]
    ) -> list[PurchaseOrderLine]:
        if not raw_lines:
            raise ValidationError("Purchase order must have at least one line")
        lines: list[PurchaseOrderLine] = []
        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")

            discount_type = raw.get("discount_type") or AmountType.PERCENT
            if isinstance(discount_type, str):
                discount_type = AmountType(discount_type)
            tax_type = raw.get("tax_type") or AmountType.PERCENT
            if isinstance(tax_type, str):
                tax_type = AmountType(tax_type)
            discount_value = Decimal(str(raw.get("discount_value", 0)))
            tax_rate = Decimal(str(raw.get("tax_rate", 0)))

            base_unit_id = raw.get("base_unit_id") or item.base_unit_id
            if base_unit_id:
                unit = await self._inventory.get_base_unit(base_unit_id, company_id)
                if not unit:
                    raise NotFoundError(f"Base unit '{base_unit_id}' not found")

            base = (qty * unit_price).quantize(Decimal("0.01"))
            if discount_type == AmountType.PERCENT:
                discount_amount = (base * discount_value / Decimal("100")).quantize(
                    Decimal("0.01")
                )
            else:
                discount_amount = discount_value.quantize(Decimal("0.01"))
            if discount_amount > base:
                raise ValidationError("Discount cannot exceed line amount")
            after_discount = (base - discount_amount).quantize(Decimal("0.01"))
            if tax_type == AmountType.PERCENT:
                tax_amount = (after_discount * tax_rate / Decimal("100")).quantize(
                    Decimal("0.01")
                )
            else:
                tax_amount = tax_rate.quantize(Decimal("0.01"))

            lines.append(
                PurchaseOrderLine(
                    line_number=index,
                    item_id=raw["item_id"],
                    item_sku=item.sku,
                    item_name=item.name,
                    description=raw.get("description") or item.name,
                    base_unit_id=base_unit_id,
                    quantity=qty,
                    unit_price=unit_price,
                    discount_type=discount_type,
                    discount_value=discount_value,
                    discount_amount=discount_amount,
                    tax_type=tax_type,
                    tax_rate=tax_rate,
                    tax_amount=tax_amount,
                    line_total=(after_discount + tax_amount).quantize(Decimal("0.01")),
                )
            )
        return lines

    def _build_bill_lines(self, raw_lines: list[dict]) -> list[VendorBillLine]:
        if not raw_lines:
            raise ValidationError("Vendor bill must have at least one line")
        lines: list[VendorBillLine] = []
        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(
                VendorBillLine(
                    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,
                    purchase_order_line_id=raw.get("purchase_order_line_id"),
                    goods_receipt_line_id=raw.get("goods_receipt_line_id"),
                )
            )
        return lines

    def _sum_line_totals(
        self, lines: list[PurchaseOrderLine]
    ) -> tuple[Decimal, Decimal, Decimal, Decimal]:
        subtotal = Decimal("0.00")
        discount_amount = Decimal("0.00")
        tax_amount = Decimal("0.00")
        for line in lines:
            base = (line.quantity * line.unit_price).quantize(Decimal("0.01"))
            subtotal += base
            discount_amount += line.discount_amount
            tax_amount += line.tax_amount
        total = (subtotal - discount_amount + tax_amount).quantize(Decimal("0.01"))
        return subtotal, discount_amount, tax_amount, total

    def _sum_bill_totals(
        self, lines: list[VendorBillLine]
    ) -> 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 _refresh_purchase_order_workflow_status(
        self, company_id: str, order_id: str
    ) -> None:
        order = await self._purchases.get_purchase_order(order_id, company_id)
        if not order or order.status in (PurchaseOrderStatus.CANCELLED, PurchaseOrderStatus.CLOSED):
            return

        bills = await self._purchases.list_vendor_bills(company_id, None, None, 0, 1000)
        po_bills = [b for b in bills if b.purchase_order_id == order_id]
        if not po_bills:
            return

        any_paid = any(b.status in (VendorBillStatus.PARTIALLY_PAID, VendorBillStatus.PAID) for b in po_bills)
        all_paid = all(b.status == VendorBillStatus.PAID for b in po_bills)
        any_posted = any(b.status in (VendorBillStatus.POSTED, VendorBillStatus.PARTIALLY_PAID, VendorBillStatus.PAID) for b in po_bills)

        if all_paid:
            order.status = PurchaseOrderStatus.PAID
        elif any_paid:
            order.status = PurchaseOrderStatus.PARTIALLY_PAID
        elif any_posted:
            order.status = PurchaseOrderStatus.INVOICED
        order.updated_at = datetime.utcnow()
        await self._purchases.update_purchase_order(order_id, order)

    async def _post_grn_accounting_entry(
        self, user: UserRegistration, company_id: str, receipt: GoodsReceipt
    ) -> None:
        grni_account = await self._accounts.get_by_code(company_id, "2120")
        if not grni_account or grni_account.is_group:
            return

        debit_entries: list[dict] = []
        total_debit = Decimal("0.00")
        missing_accounts: list[str] = []
        for line in receipt.lines:
            item = await self._inventory.get_item(line.item_id, company_id)
            if not item or not item.track_inventory:
                continue
            account_id = item.inventory_account_id
            if not account_id:
                missing_accounts.append(item.sku or item.code or item.name or line.item_id)
                continue
            amount = (line.quantity_received * line.unit_cost).quantize(Decimal("0.01"))
            if amount <= 0:
                continue
            debit_entries.append(
                {
                    "account_id": account_id,
                    "description": f"GRN {receipt.grn_number} stock in",
                    "debit_amount": amount,
                    "credit_amount": Decimal("0.00"),
                }
            )
            total_debit += amount

        if missing_accounts:
            raise ValidationError(
                "Cannot post GRN accounting: inventory account is missing for item(s): "
                + ", ".join(missing_accounts[:8])
                + ("" if len(missing_accounts) <= 8 else "…")
            )

        if total_debit <= 0:
            return

        aggregated: dict[str, dict] = {}
        for entry in debit_entries:
            key = entry["account_id"]
            if key not in aggregated:
                aggregated[key] = {
                    "account_id": key,
                    "description": entry["description"],
                    "debit_amount": Decimal("0.00"),
                    "credit_amount": Decimal("0.00"),
                }
            aggregated[key]["debit_amount"] += Decimal(str(entry["debit_amount"]))

        entries = list(aggregated.values())
        entries.append(
            {
                "account_id": grni_account.id or "",
                "description": f"GRN accrual {receipt.grn_number}",
                "debit_amount": Decimal("0.00"),
                "credit_amount": total_debit,
            }
        )

        voucher = await self._vouchers.create_voucher(
            user,
            company_id,
            {
                "voucher_type": VoucherType.PURCHASE.value,
                "voucher_date": receipt.receipt_date,
                "reference": receipt.grn_number,
                "narration": receipt.notes or f"Stock in against {receipt.grn_number}",
                "entries": entries,
            },
        )
        await self._vouchers.post_voucher(user, company_id, voucher.id or "")

    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 _is_valid_vendor_ap_account(
        self, company_id: str, account_id: str | None
    ) -> bool:
        """Accept only postable numeric COA codes (reject AP-VEN-* and other non-numeric)."""
        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
        code = str(account.code or "").strip()
        if not code.isdigit():
            return False
        return True

    async def _rename_account_to_next_vendor_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("Vendor AP account not found")
        new_code = await self._next_vendor_ap_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 vendor AP account code")
        return account_id

    async def _resolve_or_create_vendor_ap_account(
        self,
        *,
        company_id: str,
        preferred_account_id: str | None,
        vendor_code: str,
        vendor_name: str,
        opening_balance: Decimal = Decimal("0"),
    ) -> str:
        if await self._is_valid_vendor_ap_account(company_id, preferred_account_id):
            return preferred_account_id or ""

        # If UI pre-created an AP-VEN-* account, rename it to the next numeric code.
        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("AP-VEN") or not str(existing.code or "").isdigit():
                    return await self._rename_account_to_next_vendor_code(
                        company_id, preferred_account_id
                    )

        return await self._create_vendor_ap_account(
            company_id=company_id,
            vendor_code=vendor_code,
            vendor_name=vendor_name,
            opening_balance=opening_balance,
        )

    async def _resolve_vendor_ap_parent(
        self, company_id: str
    ) -> ChartOfAccount | None:
        """Prefer Vendors & Suppliers group (2130), else Accounts Payable if group."""
        for code in ("2130", "2110", "2100"):
            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_vendor_ap_code(self, company_id: str) -> str:
        """Allocate next numeric COA code under Vendors group (2130 → 2135, 2136…).

        Never uses AP-VEN-* style codes; always continues the parent's numeric series.
        """
        parent = await self._resolve_vendor_ap_parent(company_id)
        if not parent or not parent.id:
            raise ValidationError(
                "Vendors & Suppliers group account (2130) is required before creating vendor AP accounts"
            )
        if not str(parent.code or "").isdigit():
            raise ValidationError(
                f"Vendor AP parent account code '{parent.code}' must be numeric to allocate child codes"
            )

        accounts = await self._accounts.list_all(
            company_id, AccountType.LIABILITY, 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
            # Same parent level: direct children, or same numeric prefix series (213x under 2130)
            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 AP account code for vendor")

    async def _create_vendor_ap_account(
        self,
        *,
        company_id: str,
        vendor_code: str,
        vendor_name: str,
        opening_balance: Decimal = Decimal("0"),
    ) -> str:
        """Create a liability AP account for the vendor and return its id."""
        parent = await self._resolve_vendor_ap_parent(company_id)
        if not parent:
            raise ValidationError(
                "Vendors & Suppliers group account (2130) is required before creating vendor AP accounts"
            )
        code = await self._next_vendor_ap_code(company_id)
        level = parent.level + 1
        account = ChartOfAccount(
            company_id=company_id,
            code=code,
            name=f"{vendor_code} - {vendor_name}"[:200],
            account_type=AccountType.LIABILITY,
            nature=account_nature_for_type(AccountType.LIABILITY),
            parent_id=parent.id,
            level=level,
            is_group=False,
            is_active=True,
            opening_balance=opening_balance,
            current_balance=opening_balance,
            description=f"Accounts payable for vendor {vendor_code}",
        )
        created = await self._accounts.create(account)
        if not created.id:
            raise ValidationError("Failed to create AP account for vendor")
        return created.id

    async def _ensure_vendor_ap_account(self, company_id: str, vendor: Vendor) -> str | None:
        account_id = await self._resolve_or_create_vendor_ap_account(
            company_id=company_id,
            preferred_account_id=vendor.account_id,
            vendor_code=vendor.code,
            vendor_name=vendor.name,
            opening_balance=vendor.opening_balance or Decimal("0"),
        )
        if account_id and account_id != vendor.account_id:
            vendor.account_id = account_id
            vendor.updated_at = datetime.utcnow()
            if vendor.id:
                await self._purchases.update_vendor(vendor.id, vendor)
        return account_id

    async def _resolve_ap_account_id(
        self,
        *,
        company_id: str,
        vendor_id: str,
        preferred_ap_account_id: str | None,
    ) -> str | None:
        if await self._is_valid_vendor_ap_account(company_id, preferred_ap_account_id):
            return preferred_ap_account_id

        vendor = await self._purchases.get_vendor(vendor_id, company_id)
        if vendor:
            ensured = await self._ensure_vendor_ap_account(company_id, vendor)
            if ensured:
                return ensured

        # Fallback to default AP control account when vendor mapping cannot be created.
        ap_control = await self._accounts.get_by_code(company_id, "2110")
        if ap_control and not ap_control.is_group and ap_control.is_active:
            return ap_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

        # Prefer Cash for cash payments, otherwise Bank, then Cash.
        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 _get_user_company(self, user: UserRegistration, company_id: str) -> Company:
        return await resolve_company_for_user(self._companies, user, company_id)
