from decimal import Decimal
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from app.models.branch import Branch
from app.models.inventory import StockAdjustment
from app.models.product import Product, ProductUnitConversion, ProductVariant, Unit
from app.models.purchase import Purchase, PurchaseItem, PurchasePayment
from app.models.supplier import Supplier
from app.services.products import inventory_row, require_branch
from app.services.audit import record_audit

LIFECYCLES={"DRAFT", "ORDERED", "RECEIVED", "CANCELLED"}
METHODS={"CASH", "POS", "TRANSFER", "OTHER"}

def _validate_items(db, business_id, items):
    result=[]
    for item in items:
        product=db.get(Product,item.product_id)
        if not product or product.business_id != business_id or not product.is_active: raise ValueError("Invalid product.")
        variant=None
        if item.variant_id:
            variant=db.get(ProductVariant,item.variant_id)
            if not variant or variant.business_id != business_id or variant.product_id != product.id or not variant.is_active: raise ValueError("Invalid variant for product.")
        factor=Decimal("1")
        if item.unit_id:
            unit=db.get(Unit,item.unit_id)
            if not unit or not unit.is_active or (unit.business_id and unit.business_id != business_id): raise ValueError("Invalid unit.")
            conversion=db.scalar(select(ProductUnitConversion).where(ProductUnitConversion.product_id==product.id, ProductUnitConversion.unit_id==item.unit_id))
            if conversion:
                factor=conversion.quantity_in_base
            elif product.base_unit_id == item.unit_id:
                factor=Decimal("1")
            else:
                raise ValueError("Unit is not valid for this product.")
        elif product.base_unit_id is not None:
            # Omitted unit means the product's base quantity.
            factor=Decimal("1")
        quantity_base=item.quantity * factor
        result.append((item, product, variant, quantity_base, item.quantity * item.unit_cost))
    return result

def _supplier_branch(db,business_id,supplier_id,branch_id):
    supplier=db.get(Supplier,supplier_id)
    if not supplier or supplier.business_id != business_id: raise ValueError("Invalid supplier for this business.")
    require_branch(db,business_id,branch_id)

def _totals(rows, discount, tax):
    subtotal=sum((r[4] for r in rows), Decimal("0")); total=subtotal-discount+tax
    if total < 0: raise ValueError("Discount cannot exceed subtotal plus tax.")
    return subtotal,total

def create_purchase(db,business_id,user_id,data):
    if data.lifecycle_status not in {"DRAFT","ORDERED"}: raise ValueError("Purchase must start as DRAFT or ORDERED.")
    _supplier_branch(db,business_id,data.supplier_id,data.branch_id); rows=_validate_items(db,business_id,data.items); subtotal,total=_totals(rows,data.discount,data.tax)
    p=Purchase(business_id=business_id,branch_id=data.branch_id,supplier_id=data.supplier_id,reference_number=data.reference_number.strip(),purchase_date=data.purchase_date,lifecycle_status=data.lifecycle_status,payment_status="UNPAID",subtotal=subtotal,discount=data.discount,tax=data.tax,total=total,amount_paid=Decimal("0"),balance=total,notes=data.notes,created_by=user_id)
    db.add(p); db.flush(); _add_items(db,p,rows); record_audit(db,business_id,'PURCHASE_CREATED','purchase',p.id,user_id,p.branch_id,{'reference_number':p.reference_number,'total':str(p.total)}); db.commit(); db.refresh(p); return p

def _add_items(db,p,rows):
    for item,product,variant,base,line in rows: db.add(PurchaseItem(purchase_id=p.id,product_id=product.id,variant_id=variant.id if variant else None,unit_id=item.unit_id,quantity=item.quantity,quantity_in_base=base,unit_cost=item.unit_cost,line_total=line,expiry_date=item.expiry_date))

def get_purchase(db,business_id,purchase_id,lock=False):
    stmt=select(Purchase).where(Purchase.id==purchase_id,Purchase.business_id==business_id)
    if lock: stmt=stmt.with_for_update()
    p=db.scalar(stmt)
    if not p: raise LookupError("Purchase not found.")
    return p

def update_purchase(db,business_id,purchase_id,data):
    p=get_purchase(db,business_id,purchase_id,True)
    if p.lifecycle_status not in {"DRAFT","ORDERED"} or p.amount_paid != 0: raise ValueError("Purchase can no longer be changed.")
    values=data.model_dump(exclude_unset=True); items=values.pop("items",None)
    if values.get("lifecycle_status") and values["lifecycle_status"] not in {"DRAFT","ORDERED"}: raise ValueError("Invalid lifecycle status.")
    for k,v in values.items(): setattr(p,k,v)
    _supplier_branch(db,business_id,p.supplier_id,p.branch_id)
    if items is not None:
        rows=_validate_items(db,business_id,items); db.execute(delete(PurchaseItem).where(PurchaseItem.purchase_id==p.id)); _add_items(db,p,rows)
    else: rows=[(None,None,None,None,i.line_total) for i in db.scalars(select(PurchaseItem).where(PurchaseItem.purchase_id==p.id))]
    p.subtotal,p.total=_totals(rows,p.discount,p.tax); p.balance=p.total; db.commit();db.refresh(p);return p

def receive_purchase(db,business_id,user_id,purchase_id):
    p=get_purchase(db,business_id,purchase_id,True)
    if p.lifecycle_status == "CANCELLED": raise ValueError("Cancelled purchase cannot be received.")
    if p.lifecycle_status == "RECEIVED": raise ValueError("Purchase has already been received.")
    require_branch(db,business_id,p.branch_id)
    items=list(db.scalars(select(PurchaseItem).where(PurchaseItem.purchase_id==p.id)))
    # Validate historical inputs again and lock each inventory row through commit.
    for item in items:
        product=db.get(Product,item.product_id)
        if not product or product.business_id != business_id: raise ValueError("Purchase contains invalid product.")
        if item.variant_id:
            v=db.get(ProductVariant,item.variant_id)
            if not v or v.product_id != product.id or v.business_id != business_id: raise ValueError("Purchase contains invalid variant.")
        if item.unit_id and item.unit_id != product.base_unit_id and not db.scalar(select(ProductUnitConversion).where(ProductUnitConversion.product_id==product.id,ProductUnitConversion.unit_id==item.unit_id)): raise ValueError("Purchase contains invalid unit.")
    for item in sorted(items,key=lambda x: str(x.variant_id or x.product_id)):
        row=inventory_row(db,business_id,p.branch_id,item.product_id,item.variant_id,True); previous=row.quantity; row.quantity=previous+item.quantity_in_base
        if item.expiry_date: row.expiry_date=item.expiry_date
        db.add(StockAdjustment(business_id=business_id,branch_id=p.branch_id,product_id=item.product_id,variant_id=item.variant_id,quantity_changed=item.quantity_in_base,previous_quantity=previous,resulting_quantity=row.quantity,reason="RESTOCK",comment=f"Purchase receipt {p.reference_number} ({p.id})",performed_by_user_id=user_id))
    p.lifecycle_status="RECEIVED"; from datetime import datetime, timezone; p.received_at=datetime.now(timezone.utc); record_audit(db,business_id,'PURCHASE_RECEIVED','purchase',p.id,user_id,p.branch_id,{'reference_number':p.reference_number}); db.commit();db.refresh(p);return p

def record_payment(db,business_id,user_id,purchase_id,data):
    if data.payment_method not in METHODS: raise ValueError("Invalid payment method.")
    p=get_purchase(db,business_id,purchase_id,True)
    if p.lifecycle_status in {"DRAFT","ORDERED","CANCELLED"}: raise ValueError("Payments require a received purchase.")
    if data.amount > p.balance: raise ValueError("Payment exceeds remaining balance.")
    db.add(PurchasePayment(business_id=business_id,purchase_id=p.id,supplier_id=p.supplier_id,amount=data.amount,payment_method=data.payment_method,reference=data.reference,recorded_by=user_id))
    p.amount_paid += data.amount; p.balance=p.total-p.amount_paid; p.payment_status="PAID" if p.balance==0 else "PARTIALLY_PAID"; db.commit();db.refresh(p);return p

def cancel_purchase(db,business_id,purchase_id):
    p=get_purchase(db,business_id,purchase_id,True)
    if p.lifecycle_status=="RECEIVED": raise ValueError("Received purchase cannot be cancelled without an inventory reversal.")
    if p.amount_paid: raise ValueError("Paid purchase cannot be cancelled.")
    p.lifecycle_status="CANCELLED";db.commit();db.refresh(p);return p
