# /home/Autopartes/pos/services/pos_engine.py
"""POS engine: sale processing, totals calculation, pricing, cancellation.

All sale operations go through this service. Stock deductions are delegated
to inventory_engine.record_sale() — this service NEVER creates inventory
operations directly.

Monetary amounts: NUMERIC(12,2) in DB, float in Python.
Tax: 16% IVA per item (from item.tax_rate field).
"""

from datetime import datetime, timedelta
from decimal import Decimal, ROUND_HALF_UP
import threading
from flask import g
from services.audit import log_action
from services.inventory_engine import (
    record_sale as inventory_record_sale,
    record_operation,
    get_stock,
    get_stock_bulk,
)
from services.accounting_engine import record_sale_entry, record_cancellation_entry
from services.currency import convert, to_mxn, get_exchange_rate
from services.savings_engine import record_sale_savings


def _safe_g(attr, default=None):
    """Safely read flask.g attribute outside of app context."""
    try:
        return getattr(g, attr, default)
    except RuntimeError:
        return default


def _to_dec(val):
    """Convert a value to Decimal for precise arithmetic."""
    return Decimal(str(val))


def calculate_totals(items):
    """Compute subtotal, discount amounts, tax, and total for a list of items.

    Uses Python Decimal for all intermediate calculations to avoid
    floating-point accumulation errors. Each line item is rounded
    individually before summing. Converts back to float only at
    the end for JSON serialization.

    Each item dict must have: unit_price, quantity, discount_pct, tax_rate.
    Returns dict with computed values and enriched items list.

    Args:
        items: list of dicts with keys:
            - unit_price (float): price per unit
            - quantity (int): number of units
            - discount_pct (float): discount percentage (0-100)
            - tax_rate (float): tax rate as decimal (e.g., 0.16 for 16%)

    Returns:
        dict: {subtotal, discount_total, tax_total, total, items: [...enriched...]}
    """
    subtotal = Decimal('0')
    discount_total = Decimal('0')
    tax_total = Decimal('0')
    enriched_items = []
    TWO = Decimal('0.01')

    for item in items:
        qty = int(item['quantity'])
        price = _to_dec(item['unit_price'])
        discount_pct = _to_dec(item.get('discount_pct', 0))
        tax_rate = _to_dec(item.get('tax_rate', '0.16'))

        line_gross = (price * qty).quantize(TWO, rounding=ROUND_HALF_UP)
        line_discount = (line_gross * discount_pct / Decimal('100')).quantize(TWO, rounding=ROUND_HALF_UP)
        line_after_discount = (line_gross - line_discount).quantize(TWO, rounding=ROUND_HALF_UP)
        line_tax = (line_after_discount * tax_rate).quantize(TWO, rounding=ROUND_HALF_UP)
        line_subtotal = (line_after_discount + line_tax).quantize(TWO, rounding=ROUND_HALF_UP)

        subtotal += line_after_discount
        discount_total += line_discount
        tax_total += line_tax

        enriched_items.append({
            **item,
            'line_gross': float(line_gross),
            'discount_amount': float(line_discount),
            'tax_amount': float(line_tax),
            'subtotal': float(line_subtotal),
        })

    return {
        'subtotal': float(subtotal.quantize(TWO, rounding=ROUND_HALF_UP)),
        'discount_total': float(discount_total.quantize(TWO, rounding=ROUND_HALF_UP)),
        'tax_total': float(tax_total.quantize(TWO, rounding=ROUND_HALF_UP)),
        'total': float((subtotal + tax_total).quantize(TWO, rounding=ROUND_HALF_UP)),
        'items': enriched_items,
    }


def get_price_for_customer(inventory_item, customer):
    """Return the correct price based on the customer's price tier.

    Args:
        inventory_item: dict with price_1, price_2, price_3
        customer: dict with price_tier (1, 2, or 3), or None for publico general

    Returns:
        float: the applicable price
    """
    if customer is None:
        return float(inventory_item.get('price_1', 0))

    tier = customer.get('price_tier', 1)
    if tier == 3:
        price = inventory_item.get('price_3', 0)
    elif tier == 2:
        price = inventory_item.get('price_2', 0)
    else:
        price = inventory_item.get('price_1', 0)

    return float(price) if price else float(inventory_item.get('price_1', 0))


def get_margin_info(inventory_item, selling_price=None, discount_pct=0):
    """Return cost, price, margin %, and max discount without losing margin.

    Only meaningful for employees with pos.view_cost permission (checked by caller).

    Args:
        inventory_item: dict with cost, price_1 (or selling_price override)
        selling_price: override price (if None, uses price_1)
        discount_pct: current discount percentage

    Returns:
        dict: {cost, price, margin_pct, max_discount_pct}
    """
    cost = float(inventory_item.get('cost', 0))
    price = float(selling_price) if selling_price else float(inventory_item.get('price_1', 0))

    if price <= 0:
        return {'cost': cost, 'price': price, 'margin_pct': 0.0, 'max_discount_pct': 0.0}

    effective_price = price * (1 - discount_pct / 100)
    margin_pct = ((effective_price - cost) / effective_price * 100) if effective_price > 0 else 0.0

    # Max discount before margin hits zero: price * (1 - d/100) = cost => d = (1 - cost/price) * 100
    max_discount_pct = ((1 - cost / price) * 100) if price > cost else 0.0

    return {
        'cost': round(cost, 2),
        'price': round(price, 2),
        'margin_pct': round(margin_pct, 2),
        'max_discount_pct': round(max_discount_pct, 2),
    }


def process_sale(conn, sale_data):
    """Process a complete sale: validate, create records, deduct inventory, record payment.

    This is the main entry point for creating a sale. It handles the full transaction:
    1. Validate all items exist and have sufficient stock
    2. Calculate totals
    3. Create sale + sale_items records
    4. Call inventory_engine.record_sale() for each item (stock deduction)
    5. Record payment on cash register
    6. Update customer credit balance (if credit sale)
    7. Create audit log entry

    Args:
        conn: psycopg2 connection to tenant DB (caller controls commit)
        sale_data: dict with keys:
            - items: [{inventory_id, quantity, unit_price, discount_pct, tax_rate}]
            - customer_id: int or None (publico general)
            - payment_method: 'efectivo' | 'transferencia' | 'tarjeta' | 'mixto'
            - sale_type: 'cash' | 'credit' | 'mixed'
            - register_id: int (cash register session ID)
            - amount_paid: float
            - payment_details: [{method, amount, reference}] (for mixed payments)
            - notes: str (optional)

    Returns:
        dict: complete sale object with id, items, totals, change

    Raises:
        ValueError: on validation errors (insufficient stock, invalid items, etc.)
    """
    cur = conn.cursor()
    items = sale_data.get('items', [])
    customer_id = sale_data.get('customer_id')
    payment_method = sale_data.get('payment_method', 'efectivo')
    sale_type = sale_data.get('sale_type', 'cash')
    register_id = sale_data.get('register_id')
    amount_paid = float(sale_data.get('amount_paid', 0))
    payment_details = sale_data.get('payment_details', [])
    notes = sale_data.get('notes')
    branch_id = sale_data.get('branch_id') or _safe_g('branch_id')
    employee_id = _safe_g('employee_id')

    # Fallback to the main branch if none resolved (e.g. token without branch_id)
    if not branch_id:
        cur.execute("SELECT id FROM branches WHERE is_main = true AND is_active = true LIMIT 1")
        row = cur.fetchone()
        if row:
            branch_id = row[0]
        else:
            cur.execute("SELECT id FROM branches WHERE is_active = true ORDER BY id LIMIT 1")
            row = cur.fetchone()
            branch_id = row[0] if row else None

    if not branch_id:
        raise ValueError("No hay sucursal activa disponible para registrar la venta")

    # ── Multi-currency support ───────────────────────────────────────────
    currency = sale_data.get('currency', 'MXN')
    if currency not in ('MXN', 'USD'):
        raise ValueError(f"Unsupported currency: {currency}. Only MXN and USD are supported.")

    exchange_rate = sale_data.get('exchange_rate')
    if currency != 'MXN' and exchange_rate is None:
        exchange_rate = float(get_exchange_rate(conn, currency, 'MXN'))
    exchange_rate = float(exchange_rate) if exchange_rate else 1.0

    if not items:
        raise ValueError("No items in sale")

    # Validate register is open and lock it to prevent concurrent close/sale races
    if register_id:
        cur.execute("SELECT status FROM cash_registers WHERE id = %s FOR UPDATE", (register_id,))
        reg = cur.fetchone()
        if not reg or reg[0] != 'open':
            raise ValueError("Cash register is not open")

    # ─── Batch preload: inventory items + stock + customer credit ─────────
    inv_ids = [item.get('inventory_id') for item in items]
    if not inv_ids:
        raise ValueError("No items in sale")

    # Lock inventory rows to prevent race conditions on concurrent sales
    cur.execute("""
        SELECT id, part_number, name, cost, price_1, price_2, price_3,
               tax_rate, branch_id, retail_price
        FROM inventory
        WHERE id = ANY(%s) AND is_active = true
        ORDER BY id
        FOR UPDATE
    """, (inv_ids,))
    inv_rows = {r[0]: r for r in cur.fetchall()}

    # Batch stock check
    stock_map = get_stock_bulk(conn, branch_id)

    # Lock per-branch stock rows and refresh stock map to prevent overselling
    # on concurrent sales of the same items.
    if branch_id:
        cur.execute("""
            SELECT inventory_id, stock
            FROM inventory_stock
            WHERE branch_id = %s AND inventory_id = ANY(%s)
            FOR UPDATE
        """, (branch_id, inv_ids))
        stock_map = {r[0]: r[1] for r in cur.fetchall()}

    # Validate and enrich items
    enriched_items = []
    for item in items:
        inv_id = item.get('inventory_id')
        qty = int(item.get('quantity', 1))
        if qty <= 0:
            raise ValueError(f"Invalid quantity for inventory_id {inv_id}")

        inv = inv_rows.get(inv_id)
        if not inv:
            raise ValueError(f"Inventory item {inv_id} not found or inactive")

        current_stock = stock_map.get(inv_id, 0)

        # Use provided price or fetch from inventory
        unit_price = float(item.get('unit_price', inv[4]))  # default to price_1
        discount_pct = float(item.get('discount_pct', 0))
        tax_rate = float(item.get('tax_rate', inv[7] or 0.16))
        unit_cost = float(inv[3]) if inv[3] else 0

        # Validate discount against employee max
        max_discount = float(_safe_g('max_discount_pct', 100) or 100)
        if _safe_g('employee_role', 'cashier') not in ('owner', 'admin') and discount_pct > max_discount:
            raise ValueError(
                f"Discount {discount_pct}% exceeds your maximum allowed {max_discount}% "
                f"for item {inv[2]}"
            )

        enriched_items.append({
            'inventory_id': inv_id,
            'part_number': inv[1],
            'name': inv[2],
            'quantity': qty,
            'unit_price': unit_price,
            'unit_cost': unit_cost,
            'discount_pct': discount_pct,
            'tax_rate': tax_rate,
            'branch_id': inv[8],
            'stock_before': current_stock,
        })

    # Calculate totals
    totals = calculate_totals(enriched_items)

    # Validate credit sale (with row lock to prevent race conditions)
    if sale_type == 'credit' and customer_id:
        cur.execute("SELECT credit_limit, credit_balance FROM customers WHERE id = %s FOR UPDATE", (customer_id,))
        cust = cur.fetchone()
        if cust:
            credit_limit = float(cust[0] or 0)
            credit_balance = float(cust[1] or 0)
            credit_available = credit_limit - credit_balance
            if totals['total'] > credit_available and credit_limit > 0:
                raise ValueError(
                    f"Insufficient credit. Available: ${credit_available:.2f}, "
                    f"Required: ${totals['total']:.2f}"
                )

    # Calculate change
    change_given = 0.0
    if sale_type == 'cash' and payment_method == 'efectivo':
        change_given = round(max(amount_paid - totals['total'], 0), 2)

    # SAT payment method codes
    metodo_pago_sat = 'PPD' if sale_type == 'credit' else 'PUE'
    forma_pago_map = {
        'efectivo': '01', 'transferencia': '03', 'tarjeta': '04', 'mixto': '99'
    }
    forma_pago_sat = forma_pago_map.get(payment_method, '99')

    # Create sale record (with currency)
    cur.execute("""
        INSERT INTO sales
            (branch_id, customer_id, employee_id, register_id, sale_type,
             payment_method, subtotal, discount_total, tax_total, total,
             amount_paid, change_given, metodo_pago_sat, forma_pago_sat,
             status, device_id, notes, currency, exchange_rate)
        VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'completed',%s,%s,%s,%s)
        RETURNING id, created_at
    """, (
        branch_id, customer_id, employee_id, register_id, sale_type,
        payment_method, totals['subtotal'], totals['discount_total'],
        totals['tax_total'], totals['total'], amount_paid, change_given,
        metodo_pago_sat, forma_pago_sat,
        _safe_g('device_id'), notes,
        currency, exchange_rate
    ))
    sale_id, created_at = cur.fetchone()

    # Create sale items (batch insert) and deduct inventory
    sale_items_data = []
    for item in totals['items']:
        # retail_price from preloaded bulk query (index 9)
        inv = inv_rows.get(item['inventory_id'])
        retail_price = inv[9] if inv else None

        sale_items_data.append((
            sale_id, item['inventory_id'], item['part_number'], item['name'],
            item['quantity'], item['unit_price'], item.get('unit_cost', 0),
            item['discount_pct'], item['discount_amount'],
            item['tax_rate'], item['tax_amount'], item['subtotal'],
            retail_price
        ))

    cur.executemany("""
        INSERT INTO sale_items
            (sale_id, inventory_id, part_number, name, quantity,
             unit_price, unit_cost, discount_pct, discount_amount,
             tax_rate, tax_amount, subtotal, retail_price, currency, exchange_rate)
        VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
    """, [row + (currency, exchange_rate) for row in sale_items_data])

    # Deduct inventory via inventory_engine
    sale_items = []
    for item in totals['items']:
        # Pre-calculate remaining stock to avoid redundant get_stock() call
        stock_before = next((i['stock_before'] for i in enriched_items if i['inventory_id'] == item['inventory_id']), 0)
        remaining_after = stock_before - item['quantity']

        inventory_record_sale(
            conn,
            item['inventory_id'],
            item.get('branch_id', branch_id),
            item['quantity'],
            sale_id=sale_id,
            cost_at_time=item.get('unit_cost'),
            remaining_stock=remaining_after
        )

        sale_items.append({
            'inventory_id': item['inventory_id'],
            'part_number': item['part_number'],
            'name': item['name'],
            'quantity': item['quantity'],
            'unit_price': item['unit_price'],
            'unit_cost': item.get('unit_cost', 0),
            'discount_pct': item['discount_pct'],
            'discount_amount': item['discount_amount'],
            'tax_rate': item['tax_rate'],
            'tax_amount': item['tax_amount'],
            'subtotal': item['subtotal'],
        })

    # Record payment on cash register (cash movements for efectivo)
    if register_id and payment_details:
        for pd in payment_details:
            method = pd.get('method', payment_method)
            amt = float(pd.get('amount', 0))
            ref = pd.get('reference', '')
            cur.execute("""
                INSERT INTO sale_payments
                    (sale_id, register_id, method, amount, reference, currency, exchange_rate)
                VALUES (%s,%s,%s,%s,%s,%s,%s)
            """, (sale_id, register_id, method, amt, ref, currency, exchange_rate))
    elif register_id:
        cur.execute("""
            INSERT INTO sale_payments
                (sale_id, register_id, method, amount, reference, currency, exchange_rate)
            VALUES (%s,%s,%s,%s,%s,%s,%s)
        """, (sale_id, register_id, payment_method, amount_paid, sale_data.get('reference', ''), currency, exchange_rate))

    # Update customer credit balance if credit sale
    if sale_type == 'credit' and customer_id:
        cur.execute("""
            UPDATE customers
            SET credit_balance = credit_balance + %s
            WHERE id = %s
        """, (totals['total'], customer_id))

    # Audit log
    log_action(conn, 'SALE', 'sale', sale_id,
               new_value={
                   'total': totals['total'],
                   'items_count': len(sale_items),
                   'payment_method': payment_method,
                   'sale_type': sale_type,
                   'customer_id': customer_id,
               })

    cur.close()

    # Auto-generate accounting entry (non-blocking)
    # Accounting is always in MXN — convert if sale was in another currency
    try:
        total_mxn = to_mxn(totals['total'], currency, rate=exchange_rate, conn=conn)
        tax_mxn = to_mxn(totals['tax_total'], currency, rate=exchange_rate, conn=conn)
        sub_mxn = to_mxn(totals['subtotal'] - totals['discount_total'], currency, rate=exchange_rate, conn=conn)
        record_sale_entry(conn, {
            'id': sale_id,
            'sale_type': sale_type,
            'total': total_mxn,
            'tax_total': tax_mxn,
            'subtotal': sub_mxn,
            'cost_total': sum(item.get('unit_cost', 0) * item['quantity'] for item in enriched_items),
            'payment_method': payment_method,
        })
    except Exception:
        pass  # Accounting errors never block sales

    # Calculate and record savings vs retail price (non-blocking)
    try:
        record_sale_savings(conn, sale_id)
    except Exception:
        pass  # Savings errors never block sales

    # WhatsApp learning hook (non-blocking)
    try:
        from services.wa_learning import check_learning_resolution
        check_learning_resolution(sale_id, customer_id, conn)
    except Exception:
        pass  # Learning errors never block sales

    # Dropshipping webhook hook (non-blocking)
    try:
        from services import dropshipping_service as ds_svc
        from services.webhook_service import dispatch_webhooks_bulk
        webhook_urls = ds_svc.get_webhook_targets(conn, 'sale_made')
        if webhook_urls:
            payload_items = []
            for item in enriched_items:
                remaining = item['stock_before'] - item['quantity']
                payload_items.append({
                    'sku': item['part_number'],
                    'name': item['name'],
                    'quantity_sold': item['quantity'],
                    'stock_remaining': remaining,
                    'unit_price': item['unit_price'],
                })
            threading.Thread(
                target=dispatch_webhooks_bulk,
                args=(webhook_urls, 'sale_made', {
                    'sale_id': sale_id,
                    'items': payload_items,
                    'total': totals['total'],
                    'created_at': str(created_at),
                }),
                daemon=True
            ).start()
    except Exception:
        pass  # Webhook errors never block sales

    return {
        'id': sale_id,
        'branch_id': branch_id,
        'customer_id': customer_id,
        'employee_id': employee_id,
        'register_id': register_id,
        'sale_type': sale_type,
        'payment_method': payment_method,
        'subtotal': totals['subtotal'],
        'discount_total': totals['discount_total'],
        'tax_total': totals['tax_total'],
        'total': totals['total'],
        'amount_paid': amount_paid,
        'change_given': change_given,
        'metodo_pago_sat': metodo_pago_sat,
        'forma_pago_sat': forma_pago_sat,
        'status': 'completed',
        'items': sale_items,
        'created_at': str(created_at),
        'currency': currency,
        'exchange_rate': exchange_rate,
    }


def cancel_sale(conn, sale_id, reason):
    """Cancel a sale: validate permissions, reverse inventory, update credit.

    Business rules:
    - Cashiers can only cancel their own sales within 30 minutes
    - Admins and owners can cancel any sale
    - Cancelled sales are not deleted, just marked as 'cancelled'
    - Inventory is restored via RETURN operations
    - Customer credit balance is adjusted back

    Args:
        conn: psycopg2 connection
        sale_id: int
        reason: str (mandatory, min 3 chars)

    Returns:
        dict: cancellation result

    Raises:
        ValueError: on validation errors
    """
    if not reason or len(reason.strip()) < 3:
        raise ValueError("Cancellation reason is mandatory (min 3 characters)")

    cur = conn.cursor()

    # Get sale details
    cur.execute("""
        SELECT id, employee_id, customer_id, sale_type, total, status, created_at,
               branch_id, register_id
        FROM sales WHERE id = %s
    """, (sale_id,))
    sale = cur.fetchone()
    if not sale:
        raise ValueError("Sale not found")

    s_id, s_emp_id, s_cust_id, s_type, s_total, s_status, s_created, s_branch, s_register = sale

    if s_status == 'cancelled':
        raise ValueError("Sale is already cancelled")

    # Permission check: cashiers can only cancel own sales within 30 min
    role = _safe_g('employee_role', 'cashier')
    emp_id = _safe_g('employee_id')

    if role == 'cashier':
        if s_emp_id != emp_id:
            raise ValueError("Cashiers can only cancel their own sales")
        if datetime.utcnow() - s_created > timedelta(minutes=30):
            raise ValueError("Cashiers can only cancel sales within 30 minutes of creation")

    # Get sale items for inventory reversal
    cur.execute("""
        SELECT inventory_id, quantity, unit_cost
        FROM sale_items WHERE sale_id = %s
    """, (sale_id,))
    sale_items = cur.fetchall()

    # Reverse inventory: create RETURN operations (positive quantity)
    from services.inventory_engine import record_return
    for inv_id, qty, cost in sale_items:
        record_return(
            conn, inv_id, s_branch, qty,
            sale_id=sale_id,
            notes=f"Cancelacion venta #{sale_id}: {reason}"
        )

    # Update sale status
    cur.execute("""
        UPDATE sales SET status = 'cancelled', notes = COALESCE(notes || ' | ', '') || %s
        WHERE id = %s
    """, (f"CANCELADA: {reason}", sale_id))

    # Reverse accounting entry (non-blocking)
    try:
        # Fetch full sale data for the reversal entry
        cur.execute("""SELECT subtotal, tax_total, total, sale_type, payment_method
                       FROM sales WHERE id = %s""", (sale_id,))
        _sale_row = cur.fetchone()
        if _sale_row:
            record_cancellation_entry(conn, {
                'id': sale_id,
                'subtotal': float(_sale_row[0]) if _sale_row[0] else 0.0,
                'tax_total': float(_sale_row[1]) if _sale_row[1] else 0.0,
                'total': float(_sale_row[2]) if _sale_row[2] else 0.0,
                'sale_type': _sale_row[3] or 'cash',
                'payment_method': _sale_row[4] or 'efectivo',
            })
    except Exception:
        pass  # Accounting errors never block cancellations

    # Reverse customer credit balance if credit sale
    if s_type == 'credit' and s_cust_id:
        cur.execute("""
            UPDATE customers
            SET credit_balance = credit_balance - %s
            WHERE id = %s
        """, (float(s_total), s_cust_id))

    # Audit log
    log_action(conn, 'CANCEL', 'sale', sale_id,
               old_value={'status': 'completed', 'total': float(s_total)},
               new_value={'status': 'cancelled', 'reason': reason})

    # Push notification to owner/admin (best-effort, non-blocking)
    try:
        from services.push_service import notify_owner
        emp_name = _safe_g('employee_name', 'Empleado')
        notify_owner(
            conn,
            'Venta Cancelada',
            f'Venta #{sale_id} (${float(s_total):,.2f}) cancelada por {emp_name}: {reason}',
            '/pos'
        )
    except Exception:
        pass  # Push failures never block business logic

    cur.close()

    return {
        'sale_id': sale_id,
        'status': 'cancelled',
        'reason': reason,
        'items_reversed': len(sale_items),
        'total_reversed': float(s_total),
    }
