from decimal import Decimal
import secrets
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models.branch import Branch
from app.models.inventory import BranchInventory, InventoryTransfer, StockAdjustment
from app.models.product import Product, ProductCategory, ProductUnitConversion, ProductVariant, Unit
from app.models.notification import Notification
from app.services.audit import record_audit

VALID_TYPES = {"SINGLE", "PIECES_PACK", "VARIATION", "MULTI_LEVEL"}
VALID_REASONS = {"RESTOCK", "RETURN", "LOST", "DAMAGE", "DEDUCTION", "SALE", "OPENING_STOCK"}

def normalize_barcode(value):
    if value is None: return None
    value=value.strip()
    return value or None

def ean13_check_digit(body):
    total=sum(int(digit)*(1 if index%2==0 else 3) for index,digit in enumerate(body))
    return str((10-total%10)%10)

def low_stock_notification(db, business_id, branch_id, product, previous, current):
    threshold = product.low_stock_alert
    if threshold is None or not (previous > threshold and current <= threshold): return
    existing = db.scalar(select(Notification.id).where(Notification.business_id==business_id, Notification.branch_id==branch_id, Notification.category=='LOW_STOCK', Notification.related_entity_type=='product', Notification.related_entity_id==product.id, Notification.is_read.is_(False)))
    if not existing:
        db.add(Notification(business_id=business_id, branch_id=branch_id, category='LOW_STOCK', title='Low stock alert', message=f'{product.name} is at {current} (threshold {threshold}).', related_entity_type='product', related_entity_id=product.id))

def require_branch(db, business_id, branch_id):
    branch = db.get(Branch, branch_id)
    if not branch or branch.business_id != business_id or not branch.is_active: raise ValueError("Invalid branch for this business.")
    return branch
def create_product(db, business_id, data):
    if data.product_type not in VALID_TYPES: raise ValueError("Invalid product type.")
    if data.category_id:
        category = db.get(ProductCategory, data.category_id)
        if not category or category.business_id != business_id: raise ValueError("Invalid category.")
    if data.base_unit_id:
        unit = db.get(Unit, data.base_unit_id)
        if not unit or (unit.business_id and unit.business_id != business_id): raise ValueError("Invalid unit.")
    values=data.model_dump(exclude={"conversions", "variants"}); values['barcode']=normalize_barcode(values['barcode'])
    product = Product(business_id=business_id, **values)
    db.add(product); db.flush()
    for conversion in data.conversions:
        unit = db.get(Unit, conversion.unit_id)
        if not unit or (unit.business_id and unit.business_id != business_id): raise ValueError("Invalid conversion unit.")
        db.add(ProductUnitConversion(product_id=product.id, **conversion.model_dump()))
    for variant in data.variants:
        db.add(ProductVariant(product_id=product.id, business_id=business_id, **variant.model_dump()))
    record_audit(db,business_id,'PRODUCT_CREATED','product',product.id,None,None,{'name':product.name,'product_type':product.product_type}); db.commit(); db.refresh(product); return product
def product_for_business(db, business_id, product_id, lock=False):
    stmt=select(Product).where(Product.id==product_id,Product.business_id==business_id)
    return db.scalar(stmt.with_for_update() if lock else stmt)
def update_product(db,business_id,user_id,product_id,data):
    product=product_for_business(db,business_id,product_id,True)
    if not product: raise LookupError('Product not found.')
    values=data.model_dump(exclude_unset=True)
    if 'barcode' in values: values['barcode']=normalize_barcode(values['barcode'])
    for field in {'product_type','base_unit_id','track_stock'} & values.keys():
        raise ValueError(f'{field} cannot be changed after product creation.')
    if 'category_id' in values and values['category_id']:
        category=db.get(ProductCategory,values['category_id'])
        if not category or category.business_id!=business_id: raise ValueError('Invalid category.')
    changed=[]
    for field,value in values.items():
        if getattr(product,field)!=value:
            setattr(product,field,value); changed.append(field)
    if changed:
        record_audit(db,business_id,'PRODUCT_UPDATED','product',product.id,user_id,None,{'changed_fields':changed})
    db.commit();db.refresh(product);return product

def add_product_conversion(db,business_id,user_id,product_id,data):
    product=product_for_business(db,business_id,product_id,True)
    if not product:
        raise LookupError('Product not found.')
    if product.product_type not in {'PIECES_PACK','MULTI_LEVEL'}:
        raise ValueError('Unit conversions are only supported for Pieces & Pack or Multi-Level products.')
    unit=db.get(Unit,data.unit_id)
    if not unit or (unit.business_id and unit.business_id!=business_id):
        raise ValueError('Invalid conversion unit.')
    existing=db.scalar(select(ProductUnitConversion).where(
        ProductUnitConversion.product_id==product_id,
        ProductUnitConversion.unit_id==data.unit_id
    ))
    if existing:
        raise ValueError('This unit conversion already exists.')
    row=ProductUnitConversion(product_id=product_id,**data.model_dump())
    db.add(row)
    db.flush()
    record_audit(db,business_id,'PRODUCT_CONVERSION_ADDED','product',product_id,user_id,None,{
        'conversion_id':str(row.id),
        'unit_id':str(row.unit_id),
        'quantity_in_base':str(row.quantity_in_base),
    })
    db.commit()
    db.refresh(row)
    return row


def update_product_conversion(db,business_id,user_id,product_id,conversion_id,data):
    product=product_for_business(db,business_id,product_id,True)
    if not product:
        raise LookupError('Product not found.')
    row=db.get(ProductUnitConversion,conversion_id)
    if not row or row.product_id!=product_id:
        raise LookupError('Conversion not found.')
    values=data.model_dump(exclude_unset=True)
    changed=[]
    for field,value in values.items():
        if getattr(row,field)!=value:
            setattr(row,field,value)
            changed.append(field)
    if changed:
        record_audit(db,business_id,'PRODUCT_CONVERSION_UPDATED','product',product_id,user_id,None,{
            'conversion_id':str(row.id),
            'changed_fields':changed,
        })
    db.commit()
    db.refresh(row)
    return row


def add_product_variant(db,business_id,user_id,product_id,data):
    product=product_for_business(db,business_id,product_id,True)
    if not product:
        raise LookupError('Product not found.')
    if product.product_type!='VARIATION':
        raise ValueError('Variants are only supported for Variation products.')
    values=data.model_dump()
    values['barcode']=normalize_barcode(values.get('barcode'))
    row=ProductVariant(product_id=product_id,business_id=business_id,**values)
    db.add(row)
    db.flush()
    record_audit(db,business_id,'PRODUCT_VARIANT_ADDED','product',product_id,user_id,None,{
        'variant_id':str(row.id),
        'name':row.name,
    })
    db.commit()
    db.refresh(row)
    return row


def update_product_variant(db,business_id,user_id,product_id,variant_id,data):
    product=product_for_business(db,business_id,product_id,True)
    if not product:
        raise LookupError('Product not found.')
    row=db.get(ProductVariant,variant_id)
    if not row or row.product_id!=product_id or row.business_id!=business_id:
        raise LookupError('Variant not found.')
    values=data.model_dump(exclude_unset=True)
    if 'barcode' in values:
        values['barcode']=normalize_barcode(values['barcode'])
    changed=[]
    for field,value in values.items():
        if getattr(row,field)!=value:
            setattr(row,field,value)
            changed.append(field)
    if changed:
        record_audit(db,business_id,'PRODUCT_VARIANT_UPDATED','product',product_id,user_id,None,{
            'variant_id':str(row.id),
            'changed_fields':changed,
        })
    db.commit()
    db.refresh(row)
    return row


def set_product_status(db,business_id,user_id,product_id,is_active):
    product=product_for_business(db,business_id,product_id,True)
    if not product: raise LookupError('Product not found.')
    previous=product.is_active
    product.is_active=is_active
    if previous!=is_active:
        record_audit(db,business_id,'PRODUCT_STATUS_CHANGED','product',product.id,user_id,None,{'previous_status':'ACTIVE' if previous else 'INACTIVE','new_status':'ACTIVE' if is_active else 'INACTIVE'})
    db.commit();db.refresh(product);return product
def generate_product_barcode(db,business_id,user_id,product_id):
    product=product_for_business(db,business_id,product_id,True)
    if not product: raise LookupError('Product not found.')
    if product.barcode: raise ValueError('Product already has a barcode.')
    for _ in range(20):
        body=f"20{secrets.randbelow(10**10):010d}"
        barcode=body+ean13_check_digit(body)
        if not db.scalar(select(Product.id).where(Product.business_id==business_id,Product.barcode==barcode)):
            product.barcode=barcode
            record_audit(db,business_id,'PRODUCT_BARCODE_GENERATED','product',product.id,user_id,None,{'product_id':str(product.id),'barcode':barcode})
            db.commit();db.refresh(product);return product
    raise ValueError('Unable to generate a unique barcode. Please try again.')
def inventory_row(db, business_id, branch_id, product_id, variant_id=None, lock=False):
    key = str(variant_id or product_id)
    stmt = select(BranchInventory).where(BranchInventory.business_id==business_id, BranchInventory.branch_id==branch_id, BranchInventory.inventory_key==key)
    if lock: stmt = stmt.with_for_update()
    row = db.scalar(stmt)
    if not row:
        row = BranchInventory(business_id=business_id, branch_id=branch_id, product_id=product_id, variant_id=variant_id, inventory_key=key, quantity=Decimal("0")); db.add(row); db.flush()
    return row
def adjust(db, business_id, user_id, branch_id, data):
    if data.reason not in VALID_REASONS: raise ValueError("Invalid adjustment reason.")
    require_branch(db,business_id,branch_id); product=db.get(Product,data.product_id)
    if not product or product.business_id != business_id: raise ValueError("Invalid product.")
    if data.variant_id:
        v=db.get(ProductVariant,data.variant_id)
        if not v or v.product_id != product.id: raise ValueError("Invalid variant.")
    row=inventory_row(db,business_id,branch_id,product.id,data.variant_id,True); previous=row.quantity; result=previous+data.quantity_changed
    if result < 0: raise ValueError("Negative stock is not allowed.")
    row.quantity=result; low_stock_notification(db,business_id,branch_id,product,previous,result); db.add(StockAdjustment(business_id=business_id,branch_id=branch_id,product_id=product.id,variant_id=data.variant_id,quantity_changed=data.quantity_changed,previous_quantity=previous,resulting_quantity=result,reason=data.reason,comment=data.comment,performed_by_user_id=user_id)); record_audit(db,business_id,'STOCK_ADJUSTED','product',product.id,user_id,branch_id,{'reason':data.reason,'quantity_changed':str(data.quantity_changed)}); db.commit(); return row
def record_opening_stock(db,business_id,user_id,product_id,data):
    product=product_for_business(db,business_id,product_id,True)
    if not product: raise LookupError('Product not found.')
    require_branch(db,business_id,data.branch_id)
    if db.scalar(select(StockAdjustment.id).where(StockAdjustment.business_id==business_id,StockAdjustment.branch_id==data.branch_id,StockAdjustment.product_id==product_id)):
        raise ValueError('Opening stock can only be recorded before other stock activity for this product at this branch.')
    if data.variant_id:
        variant=db.get(ProductVariant,data.variant_id)
        if not variant or variant.product_id!=product_id or variant.business_id!=business_id: raise ValueError('Invalid variant.')
    row=inventory_row(db,business_id,data.branch_id,product_id,data.variant_id,True); previous=row.quantity;row.quantity=previous+data.quantity
    adjustment=StockAdjustment(business_id=business_id,branch_id=data.branch_id,product_id=product_id,variant_id=data.variant_id,quantity_changed=data.quantity,previous_quantity=previous,resulting_quantity=row.quantity,reason='OPENING_STOCK',comment=data.comment,performed_by_user_id=user_id)
    db.add(adjustment);db.flush();record_audit(db,business_id,'OPENING_STOCK_RECORDED','product',product_id,user_id,data.branch_id,{'product_id':str(product_id),'branch_id':str(data.branch_id),'entered_quantity':str(data.quantity),'normalized_base_quantity':str(data.quantity),'resulting_quantity':str(row.quantity)});db.commit();db.refresh(adjustment);return adjustment,row
def transfer(db,business_id,user_id,data):
    if data.source_branch_id == data.destination_branch_id: raise ValueError("Branches must differ.")
    require_branch(db,business_id,data.source_branch_id); require_branch(db,business_id,data.destination_branch_id)
    product=db.get(Product,data.product_id)
    if not product or product.business_id != business_id: raise ValueError("Invalid product.")
    if data.variant_id:
        variant=db.get(ProductVariant,data.variant_id)
        if not variant or variant.product_id != product.id or variant.business_id != business_id: raise ValueError("Invalid variant.")
    # Acquire rows in a stable branch order so opposite-direction transfers do
    # not deadlock. Both rows remain locked until the single transaction commits.
    branch_ids=sorted([data.source_branch_id,data.destination_branch_id],key=str)
    rows={branch_id: inventory_row(db,business_id,branch_id,product.id,data.variant_id,True) for branch_id in branch_ids}
    source=rows[data.source_branch_id]
    if source.quantity < data.quantity: raise ValueError("Insufficient stock.")
    destination=rows[data.destination_branch_id]; previous=source.quantity; source.quantity-=data.quantity; destination.quantity+=data.quantity; low_stock_notification(db,business_id,data.source_branch_id,product,previous,source.quantity)
    item=InventoryTransfer(business_id=business_id,source_branch_id=data.source_branch_id,destination_branch_id=data.destination_branch_id,product_id=product.id,variant_id=data.variant_id,quantity=data.quantity,performed_by_user_id=user_id,comment=data.comment);db.add(item);db.flush();record_audit(db,business_id,'STOCK_TRANSFERRED','inventory_transfer',item.id,user_id,data.source_branch_id,{'product_id':str(product.id),'destination_branch_id':str(data.destination_branch_id),'quantity':str(data.quantity)}); db.commit(); return source,destination
