from typing import List, Optional, Union

from sqlalchemy import or_
from sqlalchemy.orm import Session, joinedload
from app.models.landed_cost import LandedCost, LandedCostItem
from app.models.purchases import Purchase, PurchaseItem
from app.models.inventory import Product, InventoryTransaction
from app.models.branch import Branch
from app.schemas.landed_cost import LandedCostCreate
from decimal import Decimal

class LandedCostService:
    def __init__(self, db: Session):
        self.db = db

    def _company_purchase_filter(self, query, company_id: Optional[str]):
        if not company_id:
            return query
        branch_ids_sq = self.db.query(Branch.id).filter(Branch.company_id == company_id)
        return query.join(Purchase, LandedCost.purchase_id == Purchase.id).filter(
            or_(Purchase.branch_id.in_(branch_ids_sq), Purchase.branch_id.is_(None))
        )

    def list_landed_costs(
        self,
        company_id: Optional[str] = None,
        skip: int = 0,
        limit: int = 200,
    ) -> List[LandedCost]:
        query = (
            self.db.query(LandedCost)
            .options(
                joinedload(LandedCost.items),
                joinedload(LandedCost.purchase),
                joinedload(LandedCost.supplier),
            )
            .order_by(LandedCost.date.desc(), LandedCost.reference.asc())
        )
        query = self._company_purchase_filter(query, company_id)
        return query.offset(skip).limit(limit).all()

    def get_landed_cost(
        self, landed_cost_id: str, company_id: Optional[str] = None
    ) -> Optional[LandedCost]:
        query = (
            self.db.query(LandedCost)
            .options(
                joinedload(LandedCost.items),
                joinedload(LandedCost.purchase),
                joinedload(LandedCost.supplier),
            )
            .filter(LandedCost.id == landed_cost_id)
        )
        query = self._company_purchase_filter(query, company_id)
        return query.first()

    def create_landed_cost(
        self, landed_cost_data: Union[LandedCostCreate, dict]
    ) -> LandedCost:
        """
        Creates a new Landed Cost document and its associated items.
        """
        if isinstance(landed_cost_data, dict):
            landed_cost_data = LandedCostCreate(**landed_cost_data)

        if not landed_cost_data.purchase_id:
            raise ValueError("A purchase ID is required to create a landed cost document.")

        if not landed_cost_data.items:
            raise ValueError("At least one cost item is required.")

        purchase = self.db.query(Purchase).filter(Purchase.id == landed_cost_data.purchase_id).first()
        if not purchase:
            raise ValueError("Purchase not found.")

        total_amount = sum(item.amount for item in landed_cost_data.items)

        db_landed_cost = LandedCost(
            purchase_id=landed_cost_data.purchase_id,
            reference=landed_cost_data.reference,
            supplier_id=landed_cost_data.supplier_id,
            date=landed_cost_data.date,
            notes=landed_cost_data.notes,
            total_amount=total_amount,
            status="draft"
        )

        for item_data in landed_cost_data.items:
            item_payload = (
                item_data.model_dump()
                if hasattr(item_data, "model_dump")
                else dict(item_data)
            )
            db_item = LandedCostItem(**item_payload, landed_cost=db_landed_cost)
            self.db.add(db_item)

        self.db.add(db_landed_cost)
        self.db.commit()
        self.db.refresh(db_landed_cost)
        return db_landed_cost

    def allocate_landed_cost(self, landed_cost_id: str):
        """
        Allocates the total landed cost to the items of the associated purchase,
        updating the cost price of the products.
        """
        landed_cost = self.db.query(LandedCost).filter(LandedCost.id == landed_cost_id).first()
        if not landed_cost:
            raise ValueError("Landed Cost document not found.")

        if landed_cost.status == 'allocated':
            raise ValueError("Landed costs have already been allocated.")

        purchase = landed_cost.purchase
        if not purchase:
            raise ValueError("Associated purchase not found.")

        purchase_items = purchase.purchase_items
        if not purchase_items:
            return # No items to allocate to

        # For now, we'll use a simple allocation by quantity.
        # This can be expanded to support other methods (cost, weight, volume).
        total_quantity = sum(item.quantity for item in purchase_items)

        if total_quantity == 0:
            return # Avoid division by zero

        cost_per_unit_quantity = Decimal(landed_cost.total_amount) / Decimal(total_quantity)

        for item in purchase_items:
            product = self.db.query(Product).filter(Product.id == item.product_id).first()
            if not product:
                continue

            allocated_cost = cost_per_unit_quantity * Decimal(item.quantity)
            
            # The new cost of the item is its original cost + allocated landed cost
            new_item_total_cost = (Decimal(item.cost) * Decimal(item.quantity)) + allocated_cost
            new_unit_cost = new_item_total_cost / Decimal(item.quantity)

            # Update product's cost price using a weighted average
            old_total_value = Decimal(product.cost_price) * Decimal(product.quantity_in_stock)
            new_total_value = old_total_value + allocated_cost
            new_total_quantity = Decimal(product.quantity_in_stock)
            
            if new_total_quantity > 0:
                product.cost_price = new_total_value / new_total_quantity

            # Create an inventory transaction to record the cost adjustment
            transaction = InventoryTransaction(
                product_id=product.id,
                transaction_type='cost_adjustment',
                quantity=0, # No change in quantity
                unit_cost=new_unit_cost, # The new effective unit cost
                reference=f"Landed cost allocation from {landed_cost.reference}",
                related_purchase_id=purchase.id
            )
            self.db.add(transaction)

        landed_cost.status = 'allocated'
        self.db.commit()

        return True
