#!/usr/bin/env python3
"""
Script para corregir capas de valoración (SVL) faltantes en Odoo 16.
=======================================================================

PROBLEMA: Productos con stock en almacén y costo definido, pero sin capas
de valoración (stock.valuation.layer), lo que causa que el listado de
existencias muestre Costo Unitario = $0.00 y Valor Total = $0.00.

QUÉ HACE:
  - Identifica productos con stock > 0, costo > 0, pero sin SVL
  - Crea una capa de valoración inicial para cada uno
  - NO modifica cantidades, stock.quant, stock.move ni contabilidad

QUÉ NO HACE:
  - NO toca cantidades de inventario
  - NO crea movimientos de stock
  - NO genera asientos contables
  - NO modifica productos existentes

USO:
  python3 fix_svl_missing.py --dry-run    # Solo muestra lo que haría
  python3 fix_svl_missing.py --apply      # Aplica la corrección
  python3 fix_svl_missing.py --verify     # Verifica después de aplicar
"""

import xmlrpc.client
import sys
import logging
from datetime import datetime

# ─── Configuración ────────────────────────────────────────────────────────
ODOO_URL = 'http://localhost:8069'
ODOO_DB = 'hazard_new'
ODOO_USER = 'admin'
ODOO_PASS = 'admin'
COMPANY_ID = 1

# ─── Logging ──────────────────────────────────────────────────────────────
log_filename = f"/tmp/fix_svl_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler(log_filename),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)


def connect():
    """Establece conexión XML-RPC con Odoo."""
    common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
    uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASS, {})
    if not uid:
        raise ConnectionError("No se pudo autenticar con Odoo")
    models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
    logger.info("Conectado a Odoo (uid=%s, db=%s)", uid, ODOO_DB)
    return uid, models


def find_affected_products(uid, models):
    """
    Encuentra productos que tienen:
    1. stock.quant con quantity > 0 en ubicación interna
    2. standard_price > 0
    3. NO tienen stock.valuation.layer

    Retorna lista de dicts con product_id, qty, standard_price, quant_id.
    """
    logger.info("─── Buscando productos afectados ───")

    # Paso 1: Obtener todos los quants con stock positivo en ubicaciones internas
    quant_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
        'stock.quant', 'search',
        [[['quantity', '>', 0], ['location_id.usage', '=', 'internal']]])

    quants = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
        'stock.quant', 'read', [quant_ids],
        {'fields': ['product_id', 'quantity', 'location_id', 'company_id']})

    logger.info("Quants con stock positivo en almacén: %d", len(quants))

    # Paso 2: Agrupar por product_id (sumar qty si hay múltiples quants)
    product_quants = {}
    for q in quants:
        pid = q['product_id'][0]
        if pid not in product_quants:
            product_quants[pid] = {
                'product_id': pid,
                'product_name': q['product_id'][1],
                'total_qty': 0.0,
                'company_id': q['company_id'][0] if q['company_id'] else COMPANY_ID,
                'quant_ids': []
            }
        product_quants[pid]['total_qty'] += q['quantity']
        product_quants[pid]['quant_ids'].append(q['id'])

    logger.info("Productos únicos con stock: %d", len(product_quants))

    # Paso 3: Filtrar los que NO tienen SVL
    all_product_ids = list(product_quants.keys())
    svl_product_ids_raw = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
        'stock.valuation.layer', 'search_read',
        [[['product_id', 'in', all_product_ids]]],
        {'fields': ['product_id'], 'limit': 0})

    products_with_svl = set(s['product_id'][0] for s in svl_product_ids_raw)
    products_without_svl = {pid: data for pid, data in product_quants.items()
                            if pid not in products_with_svl}

    logger.info("Productos SIN SVL: %d", len(products_without_svl))

    # Paso 4: Obtener standard_price y filtrar los que tienen costo > 0
    if not products_without_svl:
        return []

    pids_to_check = list(products_without_svl.keys())

    # Leer en lotes de 200 para evitar timeout
    affected = []
    batch_size = 200
    for i in range(0, len(pids_to_check), batch_size):
        batch = pids_to_check[i:i+batch_size]
        prods = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
            'product.product', 'read', [batch],
            {'fields': ['standard_price', 'default_code', 'display_name',
                         'cost_method', 'categ_id']})

        for p in prods:
            if p['standard_price'] and p['standard_price'] > 0:
                info = products_without_svl[p['id']]
                info['standard_price'] = p['standard_price']
                info['default_code'] = p.get('default_code') or ''
                info['display_name'] = p.get('display_name') or ''
                info['cost_method'] = p.get('cost_method') or 'standard'
                info['categ_id'] = p.get('categ_id', [False, ''])[0]
                affected.append(info)

    logger.info("Productos con costo > 0 pero SIN SVL: %d", len(affected))
    return affected


def dry_run(affected):
    """Muestra lo que haría sin modificar nada."""
    logger.info("")
    logger.info("═══════════════════════════════════════════════════")
    logger.info("  DRY RUN - No se modificará nada")
    logger.info("═══════════════════════════════════════════════════")
    logger.info("")

    total_value = 0.0
    for i, item in enumerate(affected, 1):
        value = item['total_qty'] * item['standard_price']
        total_value += value
        logger.info(
            "[%3d] %-50s | Qty: %6.2f | Costo: $%10.2f | Valor: $%10.2f",
            i,
            item.get('default_code') or item.get('display_name', '')[:50],
            item['total_qty'],
            item['standard_price'],
            value
        )

    logger.info("")
    logger.info("─── RESUMEN ───")
    logger.info("Productos a corregir:  %d", len(affected))
    logger.info("Valor total faltante:  $%.2f", total_value)
    logger.info("")
    logger.info("Para aplicar la corrección ejecutar con --apply")


def apply_fix(uid, models, affected):
    """
    Crea las capas de valoración faltantes.

    Para cada producto afectado, crea UN registro en stock.valuation.layer
    que representa la entrada inicial del inventario con su costo correcto.
    """
    logger.info("")
    logger.info("═══════════════════════════════════════════════════")
    logger.info("  APLICANDO CORRECCIÓN")
    logger.info("═══════════════════════════════════════════════════")
    logger.info("")

    success_count = 0
    error_count = 0
    total_value_created = 0.0

    for i, item in enumerate(affected, 1):
        pid = item['product_id']
        qty = item['total_qty']
        cost = item['standard_price']
        value = qty * cost

        try:
            # Crear la capa de valoración
            # Estos campos replican lo que Odoo crea en un ajuste de inventario
            svl_vals = {
                'product_id': pid,
                'company_id': item['company_id'],
                'quantity': qty,
                'unit_cost': cost,
                'value': value,
                'remaining_qty': qty,
                'remaining_value': value,
                'description': 'Corrección de valoración - inventario inicial sin capa',
            }

            svl_id = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
                'stock.valuation.layer', 'create', [svl_vals])

            success_count += 1
            total_value_created += value

            if i % 25 == 0 or i <= 5:
                logger.info(
                    "[%3d/%d] ✅ SVL #%d creado para %s (qty=%.2f, costo=$%.2f, valor=$%.2f)",
                    i, len(affected), svl_id,
                    item.get('default_code') or str(pid),
                    qty, cost, value
                )

        except Exception as e:
            error_count += 1
            logger.error(
                "[%3d/%d] ❌ Error en producto %s (id=%d): %s",
                i, len(affected),
                item.get('default_code') or item.get('display_name', ''),
                pid, str(e)
            )

    logger.info("")
    logger.info("─── RESULTADO ───")
    logger.info("Exitosos:           %d", success_count)
    logger.info("Errores:            %d", error_count)
    logger.info("Valor total creado: $%.2f", total_value_created)
    logger.info("")

    return success_count, error_count


def verify(uid, models):
    """Verifica que la corrección funcionó comparando antes/después."""
    logger.info("")
    logger.info("═══════════════════════════════════════════════════")
    logger.info("  VERIFICACIÓN POST-CORRECCIÓN")
    logger.info("═══════════════════════════════════════════════════")
    logger.info("")

    # 1. Verificar el OVERGRIP BLANCO (caso original)
    test_ids = [9364, 9365, 9366]
    for pid in test_ids:
        prod = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
            'product.product', 'read', [pid],
            {'fields': ['display_name', 'standard_price', 'quantity_svl', 'value_svl']})
        if prod:
            p = prod[0]
            logger.info(
                "Producto %-50s | standard_price=$%.2f | quantity_svl=%.2f | value_svl=$%.2f",
                p['display_name'][:50], p['standard_price'],
                p['quantity_svl'], p['value_svl']
            )

        quants = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
            'stock.quant', 'search_read',
            [[['product_id', '=', pid], ['location_id.usage', '=', 'internal']]],
            {'fields': ['quantity', 'value']})
        for q in quants:
            logger.info(
                "  └─ Quant: qty=%.2f, value=$%.2f %s",
                q['quantity'], q['value'],
                '✅' if q['value'] > 0 else '❌ AÚN EN CERO'
            )

    # 2. Contar cuántos quants siguen en value=0
    zero_count = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
        'stock.quant', 'search_count',
        [[['quantity', '>', 0], ['value', '=', 0], ['location_id.usage', '=', 'internal']]])

    total_count = models.execute_kw(ODOO_DB, uid, ODOO_PASS,
        'stock.quant', 'search_count',
        [[['quantity', '>', 0], ['location_id.usage', '=', 'internal']]])

    logger.info("")
    logger.info("─── ESTADO GENERAL ───")
    logger.info("Quants con stock en almacén:      %d", total_count)
    logger.info("Quants con value = 0 (problema):  %d", zero_count)
    logger.info("Quants con value > 0 (OK):        %d", total_count - zero_count)


def main():
    if len(sys.argv) < 2 or sys.argv[1] not in ('--dry-run', '--apply', '--verify'):
        print(__doc__)
        print("Uso:")
        print("  python3 fix_svl_missing.py --dry-run    # Solo muestra lo que haría")
        print("  python3 fix_svl_missing.py --apply      # Aplica la corrección")
        print("  python3 fix_svl_missing.py --verify     # Verifica después de aplicar")
        sys.exit(1)

    mode = sys.argv[1]
    logger.info("Modo: %s", mode)
    logger.info("Log: %s", log_filename)

    uid, models_proxy = connect()

    if mode == '--verify':
        verify(uid, models_proxy)
        return

    affected = find_affected_products(uid, models_proxy)

    if not affected:
        logger.info("✅ No se encontraron productos afectados. No hay nada que corregir.")
        return

    if mode == '--dry-run':
        dry_run(affected)
    elif mode == '--apply':
        logger.info("")
        logger.info("⚠️  CONFIRMACIÓN: Se crearán %d capas de valoración.", len(affected))
        logger.info("    Respaldo previo: backups/tmp/hazard_new_pre_svl_fix_*.dump")
        logger.info("")

        success, errors = apply_fix(uid, models_proxy, affected)

        if errors == 0:
            logger.info("✅ Corrección completada sin errores.")
        else:
            logger.warning("⚠️ Corrección completada con %d errores. Revisar log.", errors)

        # Verificación automática post-aplicación
        logger.info("")
        logger.info("Ejecutando verificación automática...")
        verify(uid, models_proxy)


if __name__ == '__main__':
    main()
