"""
Marketplace B2B — service layer for bodegas, warehouse inventory and
Purchase Orders (Phase 1).

State machine:
    draft → submitted → confirmed → ready → delivered → closed
                     ↘ rejected (terminal)

Public API is grouped by concern:
  - Bodegas:      list_bodegas, get_bodega, verify_bodega
  - Inventory:    upload_inventory_csv, search_inventory
  - POs:          create_po_draft, submit_po, transition_po,
                  get_po_detail, list_pos_for_buyer, list_pos_for_seller
  - Notifications: notify_po_status_change (used internally by transition_po)

All DB calls take a `master_conn` (psycopg2 connection to nexus_autoparts).
The caller is responsible for committing and closing.
"""

import csv
import io
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Optional


# ═══════════════════════════════════════════════════════════════════════════
# STATE MACHINE
# ═══════════════════════════════════════════════════════════════════════════

PO_STATUSES = ('draft', 'submitted', 'confirmed', 'rejected', 'ready', 'delivered', 'closed')

# Map: current_status → {new_status: {actor_kinds}}
# 'buyer' = user who created the PO; 'seller' = bodega owner/user
PO_TRANSITIONS = {
    'draft':     {'submitted': {'buyer'}},
    'submitted': {'confirmed': {'seller'}, 'rejected': {'seller'}},
    'confirmed': {'ready': {'seller'}},
    'ready':     {'delivered': {'buyer', 'seller'}},
    'delivered': {'closed': {'buyer', 'seller'}},
    # terminal: rejected, closed
}


def _is_valid_transition(from_status: str, to_status: str, actor_kind: str) -> bool:
    allowed = PO_TRANSITIONS.get(from_status, {}).get(to_status)
    if not allowed:
        return False
    return actor_kind in allowed


# ═══════════════════════════════════════════════════════════════════════════
# BODEGAS
# ═══════════════════════════════════════════════════════════════════════════

def list_bodegas(master_conn, verified_only: bool = True, city: str = None) -> list[dict]:
    """Return all bodegas, optionally filtered."""
    cur = master_conn.cursor()
    clauses = []
    params = []
    if verified_only:
        clauses.append("verified = TRUE")
    if city:
        clauses.append("LOWER(city) = LOWER(%s)")
        params.append(city)
    where = "WHERE " + " AND ".join(clauses) if clauses else ""
    cur.execute(f"""
        SELECT id_bodega, name, owner_name, whatsapp_phone, email, city, state, verified
        FROM bodegas
        {where}
        ORDER BY name
    """, params)
    rows = cur.fetchall()
    cur.close()
    return [
        {
            'id_bodega': r[0], 'name': r[1], 'owner_name': r[2],
            'whatsapp_phone': r[3], 'email': r[4], 'city': r[5], 'state': r[6],
            'verified': r[7],
        }
        for r in rows
    ]


def get_bodega(master_conn, bodega_id: int) -> Optional[dict]:
    cur = master_conn.cursor()
    cur.execute("""
        SELECT id_bodega, name, owner_name, whatsapp_phone, email, city, state,
               address, verified, commission_pct
        FROM bodegas WHERE id_bodega = %s
    """, (bodega_id,))
    r = cur.fetchone()
    cur.close()
    if not r:
        return None
    return {
        'id_bodega': r[0], 'name': r[1], 'owner_name': r[2],
        'whatsapp_phone': r[3], 'email': r[4], 'city': r[5], 'state': r[6],
        'address': r[7], 'verified': r[8], 'commission_pct': float(r[9] or 0),
    }


def create_bodega(master_conn, *, name: str, whatsapp_phone: str,
                  owner_name: str = None, email: str = None,
                  city: str = None, state: str = None, address: str = None) -> int:
    """Register a new bodega (unverified by default). Admin verifies later."""
    cur = master_conn.cursor()
    cur.execute("""
        INSERT INTO bodegas (name, owner_name, whatsapp_phone, email, city, state, address)
        VALUES (%s, %s, %s, %s, %s, %s, %s)
        RETURNING id_bodega
    """, (name, owner_name, whatsapp_phone, email, city, state, address))
    bodega_id = cur.fetchone()[0]
    cur.close()
    return bodega_id


def verify_bodega(master_conn, bodega_id: int) -> bool:
    cur = master_conn.cursor()
    cur.execute("""
        UPDATE bodegas SET verified = TRUE, verified_at = NOW() WHERE id_bodega = %s
    """, (bodega_id,))
    ok = cur.rowcount > 0
    cur.close()
    return ok


# ═══════════════════════════════════════════════════════════════════════════
# INVENTORY — warehouse_inventory CSV upload + search
# ═══════════════════════════════════════════════════════════════════════════

def upload_inventory_csv(master_conn, bodega_id: int, csv_text: str) -> dict:
    """Bulk-upload a bodega's inventory from a CSV string.

    Expected columns (case-insensitive, whitespace-tolerant):
        part_number, stock, price
    Optional:
        name, min_order, warehouse_location, currency

    Resolution rules:
      - part_number matches `parts.oem_part_number` or `part_cross_references.cross_reference_number`.
      - If matched → linked to catalog (part_id set, seller fields NULL).
      - If NOT matched → created as seller listing (part_id NULL, seller_part_number set).
      - Existing rows are updated via UPSERT on the composite unique key.

    Returns a summary dict: {ok, inserted, updated, skipped, errors, oem_count, seller_count}
    """
    reader = csv.DictReader(io.StringIO(csv_text))
    # Normalize header names
    fieldnames = [f.strip().lower() for f in (reader.fieldnames or [])]

    required = {'part_number', 'stock', 'price'}
    missing = required - set(fieldnames)
    if missing:
        return {
            'ok': False,
            'error': f'Columnas faltantes en CSV: {", ".join(sorted(missing))}',
            'inserted': 0, 'updated': 0, 'skipped': 0,
        }

    # Resolve bodega → its legacy user_id (warehouse_inventory still requires it)
    cur = master_conn.cursor()
    cur.execute("SELECT id_bodega FROM bodegas WHERE id_bodega = %s", (bodega_id,))
    if not cur.fetchone():
        cur.close()
        return {'ok': False, 'error': f'bodega_id {bodega_id} no existe'}

    # Pre-load cross-reference map for fast lookup
    cur.execute("SELECT cross_reference_number, oem_part_id FROM part_cross_references")
    xref_map = {row[0].strip(): row[1] for row in cur.fetchall()}

    inserted = 0
    updated = 0
    skipped = 0
    oem_count = 0
    seller_count = 0
    errors = []

    for i, row in enumerate(reader, start=2):  # start=2 because row 1 is headers
        norm = {k.strip().lower(): (v or '').strip() for k, v in row.items()}
        part_number = norm.get('part_number', '')
        stock_str = norm.get('stock', '0')
        price_str = norm.get('price', '0')
        part_name = norm.get('name', '')

        if not part_number:
            errors.append(f'Fila {i}: part_number vacio')
            skipped += 1
            continue

        try:
            stock = int(stock_str)
            price = float(price_str)
        except ValueError:
            errors.append(f'Fila {i}: stock o price invalido')
            skipped += 1
            continue

        # Resolve part_number → part_id (OEM catalog or cross-reference)
        part_id = None
        cur.execute(
            "SELECT id_part FROM parts WHERE oem_part_number = %s LIMIT 1",
            (part_number,)
        )
        row_part = cur.fetchone()
        if row_part:
            part_id = row_part[0]
        else:
            # Try cross-reference
            xref_id = xref_map.get(part_number)
            if xref_id:
                part_id = xref_id

        # Resolve user_id from the bodega (use bodega_id as fallback if null)
        user_id = norm.get('user_id') or bodega_id  # backward compat
        try:
            user_id = int(user_id)
        except (ValueError, TypeError):
            user_id = bodega_id

        location = norm.get('warehouse_location') or 'Principal'
        currency = (norm.get('currency') or 'MXN').upper()
        min_order = int(norm.get('min_order') or 1)

        # UPSERT on composite unique (bodega_id, part_id, seller_part_number, warehouse_location)
        try:
            if part_id:
                # OEM-matched listing
                cur.execute("""
                    INSERT INTO warehouse_inventory
                        (user_id, part_id, seller_part_number, seller_part_name,
                         price, stock_quantity, min_order_quantity,
                         warehouse_location, bodega_id, currency, updated_at)
                    VALUES (%s, %s, NULL, NULL, %s, %s, %s, %s, %s, %s, NOW())
                    ON CONFLICT (bodega_id, part_id, warehouse_location) WHERE part_id IS NOT NULL
                    DO UPDATE SET
                        price = EXCLUDED.price,
                        stock_quantity = EXCLUDED.stock_quantity,
                        min_order_quantity = EXCLUDED.min_order_quantity,
                        user_id = EXCLUDED.user_id,
                        currency = EXCLUDED.currency,
                        updated_at = NOW()
                    RETURNING (xmax = 0) AS inserted
                """, (user_id, part_id, price, stock, min_order, location, bodega_id, currency))
                oem_count += 1
            else:
                # Seller listing (no catalog match)
                cur.execute("""
                    INSERT INTO warehouse_inventory
                        (user_id, part_id, seller_part_number, seller_part_name,
                         price, stock_quantity, min_order_quantity,
                         warehouse_location, bodega_id, currency, updated_at)
                    VALUES (%s, NULL, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
                    ON CONFLICT (bodega_id, seller_part_number, warehouse_location) WHERE part_id IS NULL
                    DO UPDATE SET
                        price = EXCLUDED.price,
                        stock_quantity = EXCLUDED.stock_quantity,
                        min_order_quantity = EXCLUDED.min_order_quantity,
                        seller_part_name = EXCLUDED.seller_part_name,
                        user_id = EXCLUDED.user_id,
                        currency = EXCLUDED.currency,
                        updated_at = NOW()
                    RETURNING (xmax = 0) AS inserted
                """, (user_id, part_number, part_name or part_number, price, stock, min_order, location, bodega_id, currency))
                seller_count += 1

            was_insert = cur.fetchone()[0]
            if was_insert:
                inserted += 1
            else:
                updated += 1
        except Exception as e:
            errors.append(f'Fila {i}: DB error: {str(e)[:100]}')
            skipped += 1
            master_conn.rollback()  # so next INSERTs can proceed
            continue

    cur.close()
    master_conn.commit()

    return {
        'ok': True,
        'inserted': inserted,
        'updated': updated,
        'skipped': skipped,
        'oem_count': oem_count,
        'seller_count': seller_count,
        'errors': errors[:20],  # cap to avoid huge responses
        'total_errors': len(errors),
    }


def search_inventory(master_conn, *, query: str = None, brand: str = None,
                      city: str = None, limit: int = 50) -> list[dict]:
    """Browse warehouse_inventory filtered by query / brand / city.

    Returns parts WITH stock > 0 from VERIFIED bodegas only.
    Aggregates identical parts across bodegas so the buyer sees each part once
    with a list of bodegas that have it in stock.

    Includes both OEM-matched parts (part_id IS NOT NULL) and seller listings
    (part_id IS NULL) in a single unified result set.
    """
    cur = master_conn.cursor()

    like = f'%{query}%' if query else None
    city_lower = city.lower() if city else None
    params_common = []

    # Build city filter once
    city_clause = ""
    if city_lower:
        city_clause = "AND LOWER(b.city) = LOWER(%s)"
        params_common.append(city)

    # ─── Part A: OEM-matched parts (JOIN with parts catalog) ──────────
    clauses_oem = ["wi.stock_quantity > 0", "b.verified = TRUE", "wi.part_id IS NOT NULL"]
    params_oem = []

    if query:
        clauses_oem.append("(p.oem_part_number ILIKE %s OR p.name_part ILIKE %s OR COALESCE(p.name_es, '') ILIKE %s)")
        params_oem.extend([like, like, like])

    if brand:
        clauses_oem.append("""
            EXISTS (
                SELECT 1 FROM aftermarket_parts ap
                JOIN manufacturers m ON m.id_manufacture = ap.manufacturer_id
                WHERE ap.oem_part_id = p.id_part AND UPPER(m.name_manufacture) = UPPER(%s)
            )
        """)
        params_oem.append(brand)

    where_oem = " AND ".join(clauses_oem)

    # ─── Part B: Seller listings (no parts catalog join) ──────────────
    clauses_seller = ["wi.stock_quantity > 0", "b.verified = TRUE", "wi.part_id IS NULL"]
    params_seller = []

    if query:
        clauses_seller.append("(wi.seller_part_number ILIKE %s OR wi.seller_part_name ILIKE %s)")
        params_seller.extend([like, like])

    where_seller = " AND ".join(clauses_seller)

    # Combined query with UNION ALL
    sql = f"""
        SELECT * FROM (
            -- OEM-matched parts
            SELECT
                p.id_part AS id,
                p.oem_part_number AS part_number,
                COALESCE(p.name_es, p.name_part) AS name,
                p.image_url,
                COUNT(DISTINCT b.id_bodega) AS bodega_count,
                MIN(wi.price) AS min_price,
                MAX(wi.price) AS max_price,
                SUM(wi.stock_quantity) AS total_stock,
                ARRAY_AGG(DISTINCT b.name ORDER BY b.name) AS bodega_names,
                'oem' AS listing_type
            FROM warehouse_inventory wi
            JOIN bodegas b ON b.id_bodega = wi.bodega_id
            JOIN parts p ON p.id_part = wi.part_id
            WHERE {where_oem} {city_clause}
            GROUP BY p.id_part, p.oem_part_number, p.name_es, p.name_part, p.image_url

            UNION ALL

            -- Seller listings
            SELECT
                wi.id_inventory AS id,
                wi.seller_part_number AS part_number,
                wi.seller_part_name AS name,
                NULL AS image_url,
                COUNT(DISTINCT b.id_bodega) AS bodega_count,
                MIN(wi.price) AS min_price,
                MAX(wi.price) AS max_price,
                SUM(wi.stock_quantity) AS total_stock,
                ARRAY_AGG(DISTINCT b.name ORDER BY b.name) AS bodega_names,
                'seller' AS listing_type
            FROM warehouse_inventory wi
            JOIN bodegas b ON b.id_bodega = wi.bodega_id
            WHERE {where_seller} {city_clause}
            GROUP BY wi.id_inventory, wi.seller_part_number, wi.seller_part_name
        ) combined
        ORDER BY total_stock DESC
        LIMIT %s
    """

    all_params = params_oem + params_common + params_seller + params_common + [limit]
    cur.execute(sql, all_params)

    rows = cur.fetchall()
    cur.close()

    return [
        {
            'id': r[0],
            'part_number': r[1],
            'name': r[2],
            'image_url': r[3],
            'bodega_count': r[4],
            'min_price': float(r[5]) if r[5] is not None else None,
            'max_price': float(r[6]) if r[6] is not None else None,
            'total_stock_hint': 'En stock' if (r[7] or 0) > 0 else 'Consultar',
            'bodega_names': r[8],
            'listing_type': r[9],
        }
        for r in rows
    ]


def get_bodegas_with_part(master_conn, part_id: int) -> list[dict]:
    """Return the list of verified bodegas that currently have a given OEM part
    in stock. Used when the buyer wants to pick WHICH bodega to order from.
    """
    cur = master_conn.cursor()
    cur.execute("""
        SELECT b.id_bodega, b.name, b.city, b.whatsapp_phone,
               wi.price, wi.stock_quantity, wi.min_order_quantity, wi.currency
        FROM warehouse_inventory wi
        JOIN bodegas b ON b.id_bodega = wi.bodega_id
        WHERE wi.part_id = %s AND wi.stock_quantity > 0 AND b.verified = TRUE
        ORDER BY wi.price ASC
    """, (part_id,))
    rows = cur.fetchall()
    cur.close()
    return [
        {
            'id_bodega': r[0], 'name': r[1], 'city': r[2], 'whatsapp_phone': r[3],
            'price': float(r[4]) if r[4] is not None else None,
            'stock_hint': 'En stock',  # don't expose exact quantity
            'min_order': r[6] or 1,
            'currency': r[7] or 'MXN',
        }
        for r in rows
    ]


def get_bodegas_with_listing(master_conn, wi_id: int) -> list[dict]:
    """Return the list of verified bodegas that have a specific seller listing
    (warehouse_inventory row with part_id IS NULL) in stock.
    """
    cur = master_conn.cursor()
    cur.execute("""
        SELECT b.id_bodega, b.name, b.city, b.whatsapp_phone,
               wi.price, wi.stock_quantity, wi.min_order_quantity, wi.currency
        FROM warehouse_inventory wi
        JOIN bodegas b ON b.id_bodega = wi.bodega_id
        WHERE wi.id_inventory = %s AND wi.stock_quantity > 0 AND b.verified = TRUE
        ORDER BY wi.price ASC
    """, (wi_id,))
    rows = cur.fetchall()
    cur.close()
    return [
        {
            'id_bodega': r[0], 'name': r[1], 'city': r[2], 'whatsapp_phone': r[3],
            'price': float(r[4]) if r[4] is not None else None,
            'stock_hint': 'En stock',
            'min_order': r[6] or 1,
            'currency': r[7] or 'MXN',
        }
        for r in rows
    ]


# ═══════════════════════════════════════════════════════════════════════════
# PURCHASE ORDERS
# ═══════════════════════════════════════════════════════════════════════════

def create_po_draft(master_conn, *, buyer_tenant_id: int, buyer_user_id: int,
                     buyer_display_name: str, buyer_phone: str, buyer_email: str,
                     bodega_id: int, items: list,
                     delivery_method: str = 'pickup',
                     delivery_address: str = None,
                     buyer_notes: str = None) -> int:
    """Create a PO in 'draft' status with its items.

    Args:
        items: list of dicts with keys: part_id, quantity, unit_price (optional)
               If unit_price is missing, it's pulled from warehouse_inventory.

    Returns the new po_id.
    """
    if not items:
        raise ValueError('A PO must have at least one item')

    cur = master_conn.cursor()

    # Create header
    cur.execute("""
        INSERT INTO purchase_orders (
            buyer_tenant_id, buyer_user_id, buyer_display_name, buyer_phone, buyer_email,
            bodega_id, status, delivery_method, delivery_address, buyer_notes
        ) VALUES (%s, %s, %s, %s, %s, %s, 'draft', %s, %s, %s)
        RETURNING id_po
    """, (
        buyer_tenant_id, buyer_user_id, buyer_display_name, buyer_phone, buyer_email,
        bodega_id, delivery_method, delivery_address, buyer_notes,
    ))
    po_id = cur.fetchone()[0]

    # Insert items
    total = 0.0
    for item in items:
        part_id = item.get('part_id')
        wi_id = item.get('wi_id')
        quantity = int(item['quantity'])
        if quantity < 1:
            continue

        if part_id:
            # OEM-matched part
            part_id = int(part_id)
            cur.execute("""
                SELECT p.oem_part_number, COALESCE(p.name_es, p.name_part), wi.price
                FROM parts p
                LEFT JOIN warehouse_inventory wi
                    ON wi.part_id = p.id_part AND wi.bodega_id = %s
                WHERE p.id_part = %s LIMIT 1
            """, (bodega_id, part_id))
            r = cur.fetchone()
            if not r:
                continue
            oem, name, db_price = r
            unit_price = float(item.get('unit_price') or db_price or 0)
            subtotal = round(unit_price * quantity, 2)
            total += subtotal

            cur.execute("""
                INSERT INTO purchase_order_items
                    (po_id, part_id, oem_part_number, part_name, quantity, unit_price, subtotal, notes, is_seller_listing)
                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, FALSE)
            """, (po_id, part_id, oem, name, quantity, unit_price, subtotal, item.get('notes')))

        elif wi_id:
            # Seller listing (no catalog match)
            wi_id = int(wi_id)
            cur.execute("""
                SELECT seller_part_number, seller_part_name, price
                FROM warehouse_inventory
                WHERE id_inventory = %s AND bodega_id = %s LIMIT 1
            """, (wi_id, bodega_id))
            r = cur.fetchone()
            if not r:
                continue
            seller_pn, seller_name, db_price = r
            unit_price = float(item.get('unit_price') or db_price or 0)
            subtotal = round(unit_price * quantity, 2)
            total += subtotal

            cur.execute("""
                INSERT INTO purchase_order_items
                    (po_id, part_id, oem_part_number, part_name, quantity, unit_price, subtotal, notes, is_seller_listing)
                VALUES (%s, NULL, %s, %s, %s, %s, %s, %s, TRUE)
            """, (po_id, seller_pn, seller_name or seller_pn, quantity, unit_price, subtotal, item.get('notes')))

        else:
            continue

    # Update header total
    cur.execute("UPDATE purchase_orders SET total_amount = %s WHERE id_po = %s",
                (round(total, 2), po_id))

    # Log initial status
    cur.execute("""
        INSERT INTO po_status_history (po_id, from_status, to_status, actor_user_id, actor_kind, note)
        VALUES (%s, NULL, 'draft', %s, 'buyer', 'PO creado')
    """, (po_id, buyer_user_id))

    cur.close()
    master_conn.commit()
    return po_id


def transition_po(master_conn, *, po_id: int, new_status: str,
                   actor_user_id: int, actor_kind: str,
                   note: str = None) -> dict:
    """Transition a PO to a new status with full validation and notification.

    Returns: {ok, from_status, to_status, notified}  or  {ok: False, error}
    """
    if new_status not in PO_STATUSES:
        return {'ok': False, 'error': f'Invalid status: {new_status}'}

    cur = master_conn.cursor()
    cur.execute("SELECT status FROM purchase_orders WHERE id_po = %s FOR UPDATE", (po_id,))
    row = cur.fetchone()
    if not row:
        cur.close()
        return {'ok': False, 'error': f'PO {po_id} not found'}

    from_status = row[0]
    if not _is_valid_transition(from_status, new_status, actor_kind):
        cur.close()
        return {
            'ok': False,
            'error': f'Transition {from_status}→{new_status} not allowed for {actor_kind}',
        }

    # Timestamp columns per state
    ts_field = {
        'submitted': 'submitted_at',
        'confirmed': 'confirmed_at',
        'ready': 'ready_at',
        'delivered': 'delivered_at',
        'closed': 'closed_at',
    }.get(new_status)

    if ts_field:
        cur.execute(
            f"UPDATE purchase_orders SET status = %s, {ts_field} = NOW() WHERE id_po = %s",
            (new_status, po_id),
        )
    else:
        cur.execute("UPDATE purchase_orders SET status = %s WHERE id_po = %s",
                    (new_status, po_id))

    # Log history row
    cur.execute("""
        INSERT INTO po_status_history
            (po_id, from_status, to_status, actor_user_id, actor_kind, note)
        VALUES (%s, %s, %s, %s, %s, %s)
    """, (po_id, from_status, new_status, actor_user_id, actor_kind, note))

    cur.close()
    master_conn.commit()

    # Fire notifications — non-blocking (failures logged, not raised)
    notified = []
    try:
        notified = notify_po_status_change(master_conn, po_id, new_status)
    except Exception as e:
        print(f'[marketplace] notification failed for PO {po_id}: {e}')

    return {
        'ok': True,
        'from_status': from_status,
        'to_status': new_status,
        'notified': notified,
    }


def get_po_detail(master_conn, po_id: int) -> Optional[dict]:
    cur = master_conn.cursor()
    cur.execute("""
        SELECT po.id_po, po.buyer_tenant_id, po.buyer_user_id, po.buyer_display_name,
               po.buyer_phone, po.buyer_email,
               po.bodega_id, b.name AS bodega_name, b.whatsapp_phone AS bodega_phone,
               b.email AS bodega_email,
               po.status, po.total_amount, po.currency,
               po.buyer_notes, po.seller_notes,
               po.delivery_method, po.delivery_address,
               po.created_at, po.submitted_at, po.confirmed_at, po.ready_at,
               po.delivered_at, po.closed_at
        FROM purchase_orders po
        JOIN bodegas b ON b.id_bodega = po.bodega_id
        WHERE po.id_po = %s
    """, (po_id,))
    r = cur.fetchone()
    if not r:
        cur.close()
        return None

    po = {
        'id_po': r[0], 'buyer_tenant_id': r[1], 'buyer_user_id': r[2],
        'buyer_display_name': r[3], 'buyer_phone': r[4], 'buyer_email': r[5],
        'bodega_id': r[6], 'bodega_name': r[7],
        'bodega_phone': r[8], 'bodega_email': r[9],
        'status': r[10],
        'total_amount': float(r[11]) if r[11] is not None else 0.0,
        'currency': r[12],
        'buyer_notes': r[13], 'seller_notes': r[14],
        'delivery_method': r[15], 'delivery_address': r[16],
        'created_at': r[17].isoformat() if r[17] else None,
        'submitted_at': r[18].isoformat() if r[18] else None,
        'confirmed_at': r[19].isoformat() if r[19] else None,
        'ready_at': r[20].isoformat() if r[20] else None,
        'delivered_at': r[21].isoformat() if r[21] else None,
        'closed_at': r[22].isoformat() if r[22] else None,
    }

    # Items
    cur.execute("""
        SELECT id_po_item, part_id, oem_part_number, part_name, manufacturer,
               quantity, unit_price, subtotal, confirmed_qty, notes
        FROM purchase_order_items WHERE po_id = %s ORDER BY id_po_item
    """, (po_id,))
    po['items'] = [
        {
            'id_po_item': ir[0], 'part_id': ir[1], 'oem_part_number': ir[2],
            'part_name': ir[3], 'manufacturer': ir[4],
            'quantity': ir[5],
            'unit_price': float(ir[6]) if ir[6] is not None else 0.0,
            'subtotal': float(ir[7]) if ir[7] is not None else 0.0,
            'confirmed_qty': ir[8],
            'notes': ir[9],
        }
        for ir in cur.fetchall()
    ]

    # Status history
    cur.execute("""
        SELECT from_status, to_status, actor_kind, note, created_at
        FROM po_status_history WHERE po_id = %s ORDER BY created_at
    """, (po_id,))
    po['history'] = [
        {
            'from_status': h[0], 'to_status': h[1], 'actor_kind': h[2],
            'note': h[3], 'at': h[4].isoformat() if h[4] else None,
        }
        for h in cur.fetchall()
    ]
    cur.close()
    return po


def list_pos_for_buyer(master_conn, buyer_tenant_id: int, buyer_user_id: int = None,
                        limit: int = 50) -> list[dict]:
    """Return POs created by a buyer (filtered by tenant or user)."""
    cur = master_conn.cursor()
    clauses = ['po.buyer_tenant_id = %s']
    params = [buyer_tenant_id]
    if buyer_user_id is not None:
        clauses.append('po.buyer_user_id = %s')
        params.append(buyer_user_id)
    where = ' AND '.join(clauses)
    cur.execute(f"""
        SELECT po.id_po, po.status, po.total_amount, po.currency,
               po.bodega_id, b.name AS bodega_name,
               po.created_at, po.submitted_at,
               (SELECT COUNT(*) FROM purchase_order_items WHERE po_id = po.id_po) AS item_count
        FROM purchase_orders po
        JOIN bodegas b ON b.id_bodega = po.bodega_id
        WHERE {where}
        ORDER BY po.created_at DESC
        LIMIT %s
    """, params + [limit])
    rows = cur.fetchall()
    cur.close()
    return [
        {
            'id_po': r[0], 'status': r[1],
            'total_amount': float(r[2]) if r[2] is not None else 0.0,
            'currency': r[3],
            'bodega_id': r[4], 'bodega_name': r[5],
            'created_at': r[6].isoformat() if r[6] else None,
            'submitted_at': r[7].isoformat() if r[7] else None,
            'item_count': r[8],
        }
        for r in rows
    ]


def list_pos_for_seller(master_conn, bodega_id: int, limit: int = 50) -> list[dict]:
    """Inbox: POs addressed to a seller (bodega)."""
    cur = master_conn.cursor()
    cur.execute("""
        SELECT po.id_po, po.status, po.total_amount, po.currency,
               po.buyer_tenant_id, po.buyer_display_name, po.buyer_phone,
               po.created_at, po.submitted_at,
               (SELECT COUNT(*) FROM purchase_order_items WHERE po_id = po.id_po) AS item_count
        FROM purchase_orders po
        WHERE po.bodega_id = %s AND po.status != 'draft'
        ORDER BY
          CASE po.status
            WHEN 'submitted' THEN 1
            WHEN 'confirmed' THEN 2
            WHEN 'ready' THEN 3
            ELSE 4
          END,
          po.submitted_at DESC
        LIMIT %s
    """, (bodega_id, limit))
    rows = cur.fetchall()
    cur.close()
    return [
        {
            'id_po': r[0], 'status': r[1],
            'total_amount': float(r[2]) if r[2] is not None else 0.0,
            'currency': r[3],
            'buyer_tenant_id': r[4], 'buyer_display_name': r[5], 'buyer_phone': r[6],
            'created_at': r[7].isoformat() if r[7] else None,
            'submitted_at': r[8].isoformat() if r[8] else None,
            'item_count': r[9],
        }
        for r in rows
    ]


# ═══════════════════════════════════════════════════════════════════════════
# NOTIFICATIONS — WhatsApp + Email
# ═══════════════════════════════════════════════════════════════════════════

# Per-status message templates. Each is a (subject, body) tuple.
# The body is plain text — same text goes to WA and email, with an optional
# HTML wrapper for email.
_PO_MESSAGE_TEMPLATES = {
    'submitted': (
        'Nuevo pedido Nexus #{po_id}',
        'Tienes un nuevo pedido en Nexus Marketplace.\n\n'
        'Pedido: #{po_id}\n'
        'Comprador: {buyer_display_name}\n'
        'Total: ${total_amount:,.2f} {currency}\n'
        'Items: {item_count}\n\n'
        'Entra al marketplace para confirmar o rechazar.'
    ),
    'confirmed': (
        'Pedido #{po_id} confirmado por {bodega_name}',
        'Tu pedido fue confirmado.\n\n'
        'Pedido: #{po_id}\n'
        'Bodega: {bodega_name}\n'
        'Total: ${total_amount:,.2f} {currency}\n\n'
        'Te avisaremos cuando este listo para recoger / entregar.'
    ),
    'rejected': (
        'Pedido #{po_id} rechazado',
        'Tu pedido fue rechazado por {bodega_name}.\n\n'
        'Pedido: #{po_id}\n'
        'Puedes intentar con otra bodega en el marketplace.'
    ),
    'ready': (
        'Pedido #{po_id} listo',
        'Tu pedido esta listo.\n\n'
        'Pedido: #{po_id}\n'
        'Bodega: {bodega_name}\n'
        'Metodo: {delivery_method}\n\n'
        'Pasa a recogerlo o espera la entrega.'
    ),
    'delivered': (
        'Pedido #{po_id} entregado',
        'El pedido #{po_id} fue marcado como entregado.\n'
        'Gracias por usar Nexus Marketplace.'
    ),
    'closed': (
        'Pedido #{po_id} cerrado',
        'El pedido #{po_id} fue cerrado.'
    ),
}


def notify_po_status_change(master_conn, po_id: int, new_status: str) -> list[str]:
    """Send WhatsApp + email notification about a PO status change.

    Returns a list of channel names that were successfully notified
    (e.g. ['whatsapp', 'email']). Failures are logged but not raised.
    """
    template = _PO_MESSAGE_TEMPLATES.get(new_status)
    if not template:
        return []  # no message defined for this status

    po = get_po_detail(master_conn, po_id)
    if not po:
        return []

    # Resolve context variables for the template
    ctx = {
        'po_id': po_id,
        'buyer_display_name': po.get('buyer_display_name') or 'Cliente',
        'bodega_name': po.get('bodega_name') or 'Bodega',
        'total_amount': po.get('total_amount') or 0,
        'currency': po.get('currency') or 'MXN',
        'delivery_method': po.get('delivery_method') or 'pickup',
        'item_count': len(po.get('items') or []),
    }
    subject_tpl, body_tpl = template
    try:
        subject = subject_tpl.format(**ctx)
        body = body_tpl.format(**ctx)
    except (KeyError, ValueError) as e:
        print(f'[marketplace] template format error for {new_status}: {e}')
        return []

    # Decide the recipient based on who should be notified for this status
    # - submitted → notify seller (new order arrived)
    # - confirmed/rejected/ready → notify buyer (status update)
    # - delivered → notify both (handled as 2 sends)
    # - closed → notify buyer
    recipients = []
    if new_status == 'submitted':
        recipients = [{
            'kind': 'seller',
            'phone': po.get('bodega_phone'),
            'email': po.get('bodega_email'),
        }]
    elif new_status in ('confirmed', 'rejected', 'ready', 'closed'):
        recipients = [{
            'kind': 'buyer',
            'phone': po.get('buyer_phone'),
            'email': po.get('buyer_email'),
        }]
    elif new_status == 'delivered':
        recipients = [
            {'kind': 'buyer',  'phone': po.get('buyer_phone'),  'email': po.get('buyer_email')},
            {'kind': 'seller', 'phone': po.get('bodega_phone'), 'email': po.get('bodega_email')},
        ]

    channels_used = []
    for recipient in recipients:
        # WhatsApp
        if recipient.get('phone'):
            try:
                from services import whatsapp_service
                result = whatsapp_service.send_message(recipient['phone'], body)
                if result and not result.get('error'):
                    channels_used.append(f"whatsapp:{recipient['kind']}")
            except Exception as e:
                print(f'[marketplace] WA send failed: {e}')

        # Email
        if recipient.get('email'):
            try:
                sent = _send_email(recipient['email'], subject, body)
                if sent:
                    channels_used.append(f"email:{recipient['kind']}")
            except Exception as e:
                print(f'[marketplace] email send failed: {e}')

    return channels_used


def _send_email(to_email: str, subject: str, body_text: str) -> bool:
    """Send a plain-text email via SMTP (config in pos/config.py).

    Returns True if the mail was actually sent, False if SMTP is not
    configured (silent no-op so dev environments don't crash).
    """
    import config
    if not config.SMTP_USER or not config.SMTP_PASS:
        print('[marketplace] SMTP not configured — skipping email')
        return False

    msg = MIMEMultipart('alternative')
    msg['From'] = config.SMTP_FROM
    msg['To'] = to_email
    msg['Subject'] = subject
    msg.attach(MIMEText(body_text, 'plain', 'utf-8'))

    with smtplib.SMTP(config.SMTP_HOST, config.SMTP_PORT, timeout=15) as server:
        server.starttls()
        server.login(config.SMTP_USER, config.SMTP_PASS)
        server.send_message(msg)
    print(f'[marketplace] email sent to {to_email}: {subject}')
    return True
