<?php
/**
 * Motor legal master de nómina Venezuela.
 * - Parametriza alícuotas y topes con vigencia.
 * - Corrige RPE trabajador a 0,5% y separa aportes patronales.
 * - Calcula IVSS, RPE, FAOV, INCES y Ley de Protección de Pensiones sin afectar el neto por aportes patronales.
 */
include_once('nomina_helpers.php');

if (!function_exists('nlm_column_exists')) {
    function nlm_column_exists($conexion, $table, $column) {
        return function_exists('nomina_table_has_column') ? nomina_table_has_column($conexion, $table, $column) : false;
    }
}

if (!function_exists('nlm_sql')) {
    function nlm_sql($conexion, $sql) {
        return @mysqli_query($conexion, $sql) !== false;
    }
}

if (!function_exists('nlm_ensure_master_schema')) {
    function nlm_ensure_master_schema($conexion) {
        $ok = true;

        $ok = nlm_sql($conexion, "CREATE TABLE IF NOT EXISTS nomina_parametros_legales (
            id INT AUTO_INCREMENT PRIMARY KEY,
            codigo VARCHAR(80) NOT NULL,
            nombre VARCHAR(180) NOT NULL,
            categoria VARCHAR(60) NOT NULL DEFAULT 'GENERAL',
            valor_decimal DECIMAL(18,6) NOT NULL DEFAULT 0,
            unidad VARCHAR(20) NOT NULL DEFAULT 'TASA',
            vigencia_desde DATE NOT NULL DEFAULT '2026-01-01',
            vigencia_hasta DATE DEFAULT NULL,
            activo TINYINT(1) NOT NULL DEFAULT 1,
            descripcion TEXT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            UNIQUE KEY uniq_param_vigencia (codigo, vigencia_desde),
            KEY idx_param_activo (codigo, activo, vigencia_desde)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4") && $ok;

        $ok = nlm_sql($conexion, "CREATE TABLE IF NOT EXISTS nomina_auditoria (
            id INT AUTO_INCREMENT PRIMARY KEY,
            entidad VARCHAR(80) NOT NULL,
            entidad_id INT NULL,
            accion VARCHAR(80) NOT NULL,
            usuario VARCHAR(120) NULL,
            detalle JSON NULL,
            ip VARCHAR(64) NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            KEY idx_entidad (entidad, entidad_id),
            KEY idx_accion_fecha (accion, created_at)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4") && $ok;

        $ok = nlm_sql($conexion, "CREATE TABLE IF NOT EXISTS nomina_corrida_estado_historial (
            id INT AUTO_INCREMENT PRIMARY KEY,
            corrida_id INT NOT NULL,
            estado_anterior VARCHAR(30) NULL,
            estado_nuevo VARCHAR(30) NOT NULL,
            usuario VARCHAR(120) NULL,
            motivo VARCHAR(255) NULL,
            ip VARCHAR(64) NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            KEY idx_corrida (corrida_id),
            KEY idx_estado_fecha (estado_nuevo, created_at)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4") && $ok;


        $ok = nlm_sql($conexion, "CREATE TABLE IF NOT EXISTS nomina_corrida_trabajador_snapshot (
            id INT AUTO_INCREMENT PRIMARY KEY,
            corrida_id INT NOT NULL,
            persona_id INT NOT NULL,
            nombre VARCHAR(255) NULL,
            apellido VARCHAR(255) NULL,
            cedula VARCHAR(50) NULL,
            cargo VARCHAR(120) NULL,
            id_cargo INT NULL,
            fecha_ingreso DATE NULL,
            estatus_laboral VARCHAR(30) NULL,
            salario_origen VARCHAR(30) NULL,
            salario_moneda VARCHAR(10) NULL,
            salario_monto_original DECIMAL(18,4) NOT NULL DEFAULT 0,
            salario_mensual_bs DECIMAL(18,4) NOT NULL DEFAULT 0,
            salario_diario_bs DECIMAL(18,4) NOT NULL DEFAULT 0,
            salario_hora_bs DECIMAL(18,4) NOT NULL DEFAULT 0,
            bono_extra_usd_mensual DECIMAL(18,4) NOT NULL DEFAULT 0,
            tasa_bcv_usada DECIMAL(18,6) NOT NULL DEFAULT 0,
            snapshot_json JSON NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            UNIQUE KEY uniq_corrida_persona_snapshot (corrida_id, persona_id),
            KEY idx_persona (persona_id),
            KEY idx_corrida (corrida_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4") && $ok;

        $ok = nlm_sql($conexion, "CREATE TABLE IF NOT EXISTS nomina_corrida_conceptos_snapshot (
            id INT AUTO_INCREMENT PRIMARY KEY,
            corrida_id INT NOT NULL,
            concepto_id INT NOT NULL,
            codigo VARCHAR(30) NOT NULL,
            nombre VARCHAR(180) NOT NULL,
            tipo VARCHAR(20) NOT NULL,
            incide_salario_normal TINYINT(1) NOT NULL DEFAULT 0,
            incide_base_cotizacion TINYINT(1) NOT NULL DEFAULT 0,
            modo_calculo VARCHAR(30) NULL,
            tasa DECIMAL(18,6) NULL,
            monto_fijo DECIMAL(18,4) NULL,
            moneda_monto_fijo VARCHAR(10) NULL,
            aplica_global TINYINT(1) NOT NULL DEFAULT 0,
            activo TINYINT(1) NOT NULL DEFAULT 1,
            regla_json JSON NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            UNIQUE KEY uniq_corrida_concepto_snapshot (corrida_id, concepto_id),
            KEY idx_corrida_codigo (corrida_id, codigo),
            KEY idx_concepto (concepto_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4") && $ok;

        $ok = nlm_sql($conexion, "CREATE TABLE IF NOT EXISTS nomina_corrida_ajustes (
            id INT AUTO_INCREMENT PRIMARY KEY,
            corrida_id INT NOT NULL,
            periodo_id INT NULL,
            persona_id INT NULL,
            concepto_id INT NULL,
            tipo VARCHAR(30) NOT NULL DEFAULT 'AJUSTE',
            signo VARCHAR(10) NOT NULL DEFAULT 'AJUSTE',
            monto DECIMAL(18,2) NOT NULL DEFAULT 0,
            cantidad DECIMAL(18,4) NULL,
            motivo VARCHAR(255) NOT NULL,
            referencia VARCHAR(120) NULL,
            estado VARCHAR(30) NOT NULL DEFAULT 'REGISTRADO',
            usuario VARCHAR(120) NULL,
            ip VARCHAR(64) NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            KEY idx_corrida (corrida_id),
            KEY idx_periodo (periodo_id),
            KEY idx_persona (persona_id),
            KEY idx_tipo_estado (tipo, estado)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4") && $ok;

        $reversoCols = [
            'tipo_corrida' => "ALTER TABLE nomina_corridas ADD COLUMN tipo_corrida VARCHAR(30) NOT NULL DEFAULT 'ORDINARIA' AFTER estado",
            'corrida_origen_id' => "ALTER TABLE nomina_corridas ADD COLUMN corrida_origen_id INT NULL AFTER tipo_corrida",
            'bloqueada' => "ALTER TABLE nomina_corridas ADD COLUMN bloqueada TINYINT(1) NOT NULL DEFAULT 0 AFTER corrida_origen_id",
        ];
        foreach ($reversoCols as $col => $ddl) {
            if (!nlm_column_exists($conexion, 'nomina_corridas', $col)) {
                nlm_sql($conexion, $ddl);
            }
        }

        // Estados extendidos para flujo master. Si falla por versión MySQL, el sistema sigue operando con estados existentes.
        nlm_sql($conexion, "ALTER TABLE nomina_corridas MODIFY estado ENUM('BORRADOR','CALCULADA','REVISADA','APROBADA','PAGADA','CERRADA','ANULADA') NOT NULL DEFAULT 'BORRADOR'");

        $corridaCols = [
            'legal_snapshot' => "ALTER TABLE nomina_corridas ADD COLUMN legal_snapshot JSON NULL AFTER cestaticket_usd_mensual",
            'revisada_at' => "ALTER TABLE nomina_corridas ADD COLUMN revisada_at DATETIME NULL AFTER legal_snapshot",
            'revisada_por' => "ALTER TABLE nomina_corridas ADD COLUMN revisada_por VARCHAR(120) NULL AFTER revisada_at",
            'aprobada_at' => "ALTER TABLE nomina_corridas ADD COLUMN aprobada_at DATETIME NULL AFTER revisada_por",
            'aprobada_por' => "ALTER TABLE nomina_corridas ADD COLUMN aprobada_por VARCHAR(120) NULL AFTER aprobada_at",
            'pagada_at' => "ALTER TABLE nomina_corridas ADD COLUMN pagada_at DATETIME NULL AFTER aprobada_por",
            'pagada_por' => "ALTER TABLE nomina_corridas ADD COLUMN pagada_por VARCHAR(120) NULL AFTER pagada_at",
            'cerrada_at' => "ALTER TABLE nomina_corridas ADD COLUMN cerrada_at DATETIME NULL AFTER pagada_por",
            'cerrada_por' => "ALTER TABLE nomina_corridas ADD COLUMN cerrada_por VARCHAR(120) NULL AFTER cerrada_at",
            'estado_motivo' => "ALTER TABLE nomina_corridas ADD COLUMN estado_motivo VARCHAR(255) NULL AFTER cerrada_por",
        ];
        foreach ($corridaCols as $col => $ddl) {
            if (!nlm_column_exists($conexion, 'nomina_corridas', $col)) {
                nlm_sql($conexion, $ddl);
            }
        }

        $detalleCols = [
            'base_salario_normal' => "ALTER TABLE nomina_detalle ADD COLUMN base_salario_normal DECIMAL(18,2) NOT NULL DEFAULT 0.00 AFTER neto_pagar",
            'base_salario_integral' => "ALTER TABLE nomina_detalle ADD COLUMN base_salario_integral DECIMAL(18,2) NOT NULL DEFAULT 0.00 AFTER base_salario_normal",
            'total_patronal' => "ALTER TABLE nomina_detalle ADD COLUMN total_patronal DECIMAL(18,2) NOT NULL DEFAULT 0.00 AFTER base_salario_integral",
        ];
        foreach ($detalleCols as $col => $ddl) {
            if (!nlm_column_exists($conexion, 'nomina_detalle', $col)) {
                nlm_sql($conexion, $ddl);
            }
        }

        if (function_exists('nomina_ensure_conceptos_fixed_schema')) {
            nomina_ensure_conceptos_fixed_schema($conexion);
        }

        nlm_seed_legal_params($conexion);
        nlm_seed_legal_concepts($conexion);
        return $ok;
    }
}

if (!function_exists('nlm_seed_param')) {
    function nlm_seed_param($conexion, $codigo, $nombre, $categoria, $valor, $unidad, $desde, $descripcion) {
        $codigoSql = mysqli_real_escape_string($conexion, $codigo);
        $nombreSql = mysqli_real_escape_string($conexion, $nombre);
        $catSql = mysqli_real_escape_string($conexion, $categoria);
        $unidadSql = mysqli_real_escape_string($conexion, $unidad);
        $desdeSql = mysqli_real_escape_string($conexion, $desde);
        $descSql = mysqli_real_escape_string($conexion, $descripcion);
        $valor = (float)$valor;

        // Compatible MariaDB/MySQL: no usar INSERT ... alias de inserción con actualización automática.
        // Si el parametro ya existe, se actualiza la metadata y se preserva valor_decimal
        // para no pisar ajustes hechos por RRHH/contabilidad en pantalla.
        $exists = mysqli_query($conexion, "SELECT id FROM nomina_parametros_legales WHERE codigo = '{$codigoSql}' AND vigencia_desde = '{$desdeSql}' LIMIT 1");
        if ($exists && mysqli_num_rows($exists) > 0) {
            $row = mysqli_fetch_assoc($exists);
            $id = (int)($row['id'] ?? 0);
            return nlm_sql($conexion, "UPDATE nomina_parametros_legales SET nombre = '{$nombreSql}', categoria = '{$catSql}', unidad = '{$unidadSql}', descripcion = '{$descSql}', activo = 1 WHERE id = {$id} LIMIT 1");
        }

        $sql = "INSERT INTO nomina_parametros_legales
                (codigo, nombre, categoria, valor_decimal, unidad, vigencia_desde, activo, descripcion)
                VALUES ('{$codigoSql}', '{$nombreSql}', '{$catSql}', {$valor}, '{$unidadSql}', '{$desdeSql}', 1, '{$descSql}')";
        return nlm_sql($conexion, $sql);
    }
}

if (!function_exists('nlm_seed_legal_params')) {
    function nlm_seed_legal_params($conexion) {
        $desde = '2026-01-01';
        nlm_seed_param($conexion, 'SALARIO_MINIMO_MENSUAL_VES', 'Salario mínimo mensual base legal', 'BASES', 130.000000, 'VES', $desde, 'Valor editable. Se usa para topes IVSS/RPE. Actualizar cuando exista cambio oficial.');
        nlm_seed_param($conexion, 'IVSS_TRABAJADOR_TASA', 'IVSS trabajador', 'IVSS', 0.040000, 'TASA', $desde, 'Retención trabajador.');
        nlm_seed_param($conexion, 'IVSS_PATRONO_RIESGO_TASA', 'IVSS patronal según riesgo', 'IVSS', 0.090000, 'TASA', $desde, 'Riesgo mínimo por defecto: 9%. Cambiar a 10% o 11% si corresponde.');
        nlm_seed_param($conexion, 'IVSS_TOPE_SALARIOS_MINIMOS', 'Tope IVSS en salarios mínimos', 'IVSS', 5.000000, 'MULTIPLO', $desde, 'Tope de cotización IVSS.');
        nlm_seed_param($conexion, 'RPE_TRABAJADOR_TASA', 'RPE trabajador', 'RPE', 0.005000, 'TASA', $desde, '20% de la cotización total de 2,5%; equivale a 0,5%.');
        nlm_seed_param($conexion, 'RPE_PATRONO_TASA', 'RPE patronal', 'RPE', 0.020000, 'TASA', $desde, '80% de la cotización total de 2,5%; equivale a 2%.');
        nlm_seed_param($conexion, 'RPE_TOPE_SALARIOS_MINIMOS', 'Tope RPE en salarios mínimos', 'RPE', 10.000000, 'MULTIPLO', $desde, 'Tope superior de base RPE.');
        nlm_seed_param($conexion, 'RPE_MIN_SALARIOS_MINIMOS', 'Mínimo RPE en salarios mínimos', 'RPE', 1.000000, 'MULTIPLO', $desde, 'Tope inferior de base RPE.');
        nlm_seed_param($conexion, 'FAOV_TRABAJADOR_TASA', 'FAOV trabajador', 'FAOV', 0.010000, 'TASA', $desde, 'Retención trabajador.');
        nlm_seed_param($conexion, 'FAOV_PATRONO_TASA', 'FAOV patronal', 'FAOV', 0.020000, 'TASA', $desde, 'Aporte patronal.');
        nlm_seed_param($conexion, 'INCES_PATRONO_TASA', 'INCES patronal', 'INCES', 0.020000, 'TASA', $desde, 'Aporte patronal causado sobre salario normal; su declaración suele ser trimestral.');
        nlm_seed_param($conexion, 'INCES_TRABAJADOR_UTILIDADES_TASA', 'INCES trabajador sobre utilidades', 'INCES', 0.005000, 'TASA', $desde, 'Retención sobre utilidades/bonificaciones de fin de año, no sobre nómina ordinaria.');
        nlm_seed_param($conexion, 'PENSIONES_PATRONO_TASA', 'Ley Protección Pensiones patronal', 'PENSIONES', 0.090000, 'TASA', $desde, 'Contribución especial patronal. Parametrizable por posible actualización anual.');
        nlm_seed_param($conexion, 'PENSIONES_BASE_MINIMA_USD', 'Base mínima mensual pensiones en USD', 'PENSIONES', 240.000000, 'USD', '2026-05-01', 'Ingreso mínimo integral indexado referencial. Revisar/actualizar según obligación aplicable.');
        nlm_seed_param($conexion, 'PENSIONES_APLICAR_BASE_MINIMA', 'Aplicar base mínima pensiones', 'PENSIONES', 1.000000, 'BOOLEAN', '2026-05-01', '1 aplica base mínima mensual prorrateada por período; 0 usa solo pagos reales.');
        nlm_seed_param($conexion, 'BONO_NOCTURNO_TASA', 'Bono nocturno recargo legal', 'JORNADA', 0.300000, 'TASA', $desde, 'Recargo mínimo 30% sobre salario normal/hora para jornada nocturna ordinaria. Art. 117 LOTTT.');
        nlm_seed_param($conexion, 'NOMINA_DIAS_BASE_MENSUAL', 'Días base mensual', 'BASES', 30.000000, 'DIAS', $desde, 'Base de prorrata mensual para nómina.');
    }
}

if (!function_exists('nlm_seed_concept')) {
    function nlm_seed_concept($conexion, $codigo, $nombre, $tipo, $incideNormal, $incideBase, $modo, $tasa, $descripcionJson = null, $forceUpdateRate = false) {
        $codigoSql = mysqli_real_escape_string($conexion, $codigo);
        $nombreSql = mysqli_real_escape_string($conexion, $nombre);
        $tipoSql = mysqli_real_escape_string($conexion, $tipo);
        $modoSql = mysqli_real_escape_string($conexion, $modo);
        $tasaSql = $tasa === null ? 'NULL' : (string)(float)$tasa;
        $jsonSql = $descripcionJson === null ? 'NULL' : "'" . mysqli_real_escape_string($conexion, json_encode($descripcionJson, JSON_UNESCAPED_UNICODE)) . "'";
        $incideNormal = (int)$incideNormal;
        $incideBase = (int)$incideBase;

        // Compatible MariaDB/MySQL: UPDATE o INSERT explicito. Sin alias de inserción ni actualización automática.
        $exists = mysqli_query($conexion, "SELECT id, tasa FROM conceptos_nomina WHERE codigo = '{$codigoSql}' LIMIT 1");
        if ($exists && mysqli_num_rows($exists) > 0) {
            $row = mysqli_fetch_assoc($exists);
            $id = (int)($row['id'] ?? 0);
            $tasaUpdate = $forceUpdateRate ? $tasaSql : "COALESCE(tasa, {$tasaSql})";
            $sql = "UPDATE conceptos_nomina SET
                        nombre = '{$nombreSql}',
                        tipo = '{$tipoSql}',
                        incide_salario_normal = {$incideNormal},
                        incide_base_cotizacion = {$incideBase},
                        modo_calculo = '{$modoSql}',
                        tasa = {$tasaUpdate},
                        regla_json = {$jsonSql},
                        activo = 1
                    WHERE id = {$id} LIMIT 1";
            return nlm_sql($conexion, $sql);
        }

        $sql = "INSERT INTO conceptos_nomina
                (codigo, nombre, tipo, incide_salario_normal, incide_base_cotizacion, modo_calculo, tasa, monto_fijo, moneda_monto_fijo, aplica_global, regla_json, activo)
                VALUES ('{$codigoSql}', '{$nombreSql}', '{$tipoSql}', {$incideNormal}, {$incideBase}, '{$modoSql}', {$tasaSql}, NULL, 'VES', 0, {$jsonSql}, 1)";
        return nlm_sql($conexion, $sql);
    }
}

if (!function_exists('nlm_seed_legal_concepts')) {
    function nlm_seed_legal_concepts($conexion) {
        nlm_seed_concept($conexion, 'A-011', 'Bono nocturno ordinario 30%', 'ASIG', 1, 1, 'CANTIDAD', 0.300000, ['legal'=>'Art. 117 LOTTT: recargo jornada nocturna ordinaria'], true);
        nlm_seed_concept($conexion, 'B-001', 'Retención Seguro Social Obligatorio (S.S.O)', 'DED', 0, 1, 'TASA', 0.040000, ['legal'=>'IVSS trabajador'], true);
        nlm_seed_concept($conexion, 'B-002', 'Retención Régimen Prestacional de Empleo (R.P.E)', 'DED', 0, 1, 'TASA', 0.005000, ['legal'=>'RPE trabajador 0.5%'], true);
        nlm_seed_concept($conexion, 'B-003', 'Régimen Prestacional de Vivienda y Hábitat (F.A.O.V)', 'DED', 0, 1, 'TASA', 0.010000, ['legal'=>'FAOV trabajador'], true);
        nlm_seed_concept($conexion, 'B-008', 'Retención INCES trabajador sobre utilidades', 'DED', 0, 0, 'TASA', 0.005000, ['legal'=>'Solo utilidades/bonificaciones de fin de año'], true);
        nlm_seed_concept($conexion, 'P-001', 'Aporte patronal IVSS', 'PATR', 0, 0, 'TASA', 0.090000, ['legal'=>'IVSS patronal por riesgo'], true);
        nlm_seed_concept($conexion, 'P-002', 'Aporte patronal RPE', 'PATR', 0, 0, 'TASA', 0.020000, ['legal'=>'RPE patronal'], true);
        nlm_seed_concept($conexion, 'P-003', 'Aporte patronal FAOV', 'PATR', 0, 0, 'TASA', 0.020000, ['legal'=>'FAOV patronal'], true);
        nlm_seed_concept($conexion, 'P-004', 'Aporte patronal INCES', 'PATR', 0, 0, 'TASA', 0.020000, ['legal'=>'INCES patronal'], true);
        nlm_seed_concept($conexion, 'P-005', 'Contribución Especial Protección de Pensiones', 'PATR', 0, 0, 'TASA', 0.090000, ['legal'=>'Ley de Protección de Pensiones'], true);
    }
}

if (!function_exists('nlm_param')) {
    function nlm_param($conexion, $codigo, $fecha = null, $default = 0.0) {
        $codigoSql = mysqli_real_escape_string($conexion, $codigo);
        $fecha = $fecha ? nomina_date_to_iso($fecha) : date('Y-m-d');
        if ($fecha === '') { $fecha = date('Y-m-d'); }
        $fechaSql = mysqli_real_escape_string($conexion, $fecha);
        $sql = "SELECT valor_decimal FROM nomina_parametros_legales
                WHERE codigo = '{$codigoSql}'
                  AND activo = 1
                  AND vigencia_desde <= '{$fechaSql}'
                  AND (vigencia_hasta IS NULL OR vigencia_hasta >= '{$fechaSql}')
                ORDER BY vigencia_desde DESC, id DESC
                LIMIT 1";
        $res = mysqli_query($conexion, $sql);
        if ($res && mysqli_num_rows($res)) {
            $row = mysqli_fetch_assoc($res);
            return (float)$row['valor_decimal'];
        }
        return (float)$default;
    }
}

if (!function_exists('nlm_period_factor')) {
    function nlm_period_factor(array $periodo, $diasBase = 30.0) {
        $desde = nomina_date_to_iso($periodo['fecha_desde'] ?? '');
        $hasta = nomina_date_to_iso($periodo['fecha_hasta'] ?? '');
        if ($desde === '' || $hasta === '') { return 1.0; }
        try {
            $d1 = new DateTime($desde);
            $d2 = new DateTime($hasta);
            if ($d2 < $d1) { return 1.0; }
            $days = ((int)$d1->diff($d2)->days) + 1;
            return max(0.0, $days / max(1.0, (float)$diasBase));
        } catch (Exception $e) {
            return 1.0;
        }
    }
}

if (!function_exists('nlm_count_mondays')) {
    function nlm_count_mondays(array $periodo) {
        $desde = nomina_date_to_iso($periodo['fecha_desde'] ?? '');
        $hasta = nomina_date_to_iso($periodo['fecha_hasta'] ?? '');
        if ($desde === '' || $hasta === '') { return 0; }
        try {
            $d = new DateTime($desde);
            $h = new DateTime($hasta);
            if ($h < $d) { return 0; }
            $h->modify('+1 day');
            $period = new DatePeriod($d, new DateInterval('P1D'), $h);
            $count = 0;
            foreach ($period as $day) {
                if ((int)$day->format('N') === 1) { $count++; }
            }
            return $count;
        } catch (Exception $e) {
            return 0;
        }
    }
}

if (!function_exists('nlm_cap_base')) {
    function nlm_cap_base($base, $min, $max) {
        $base = max(0.0, (float)$base);
        if ($base <= 0) { return 0.0; }
        if ($min > 0 && $base < $min) { $base = $min; }
        if ($max > 0 && $base > $max) { $base = $max; }
        return $base;
    }
}

if (!function_exists('nlm_calc_legal_amounts')) {
    function nlm_calc_legal_amounts($conexion, array $periodo, array $bases, $tasaBcv) {
        $fechaPago = $bases['fecha_pago'] ?? ($periodo['fecha_hasta'] ?? date('Y-m-d'));
        $diasBase = nlm_param($conexion, 'NOMINA_DIAS_BASE_MENSUAL', $fechaPago, 30.0);
        $factor = nlm_period_factor($periodo, $diasBase);
        $salMin = nlm_param($conexion, 'SALARIO_MINIMO_MENSUAL_VES', $fechaPago, 130.0);
        $baseNormal = max(0.0, (float)($bases['base_normal_periodo'] ?? 0));
        $baseIntegral = max(0.0, (float)($bases['base_integral_periodo'] ?? $baseNormal));
        if ($baseIntegral <= 0) { $baseIntegral = $baseNormal; }
        $totalAsignaciones = max(0.0, (float)($bases['total_asignaciones'] ?? 0));
        $salarioMensualNormal = max(0.0, (float)($bases['salario_mensual_normal_bs'] ?? 0));
        if ($salarioMensualNormal <= 0 && $factor > 0) { $salarioMensualNormal = $baseNormal / $factor; }

        $ivssTrabTasa = nlm_param($conexion, 'IVSS_TRABAJADOR_TASA', $fechaPago, 0.04);
        $ivssPatTasa = nlm_param($conexion, 'IVSS_PATRONO_RIESGO_TASA', $fechaPago, 0.09);
        $ivssTope = nlm_param($conexion, 'IVSS_TOPE_SALARIOS_MINIMOS', $fechaPago, 5.0);
        $ivssBaseMensual = min($salarioMensualNormal, $salMin * $ivssTope);
        if ($ivssBaseMensual < 0) { $ivssBaseMensual = 0; }
        $lunes = nlm_count_mondays($periodo);
        $ivssBaseSemanal = ($ivssBaseMensual * 12.0) / 52.0;
        $ivssBasePeriodo = $ivssBaseSemanal * $lunes;

        $rpeTrabTasa = nlm_param($conexion, 'RPE_TRABAJADOR_TASA', $fechaPago, 0.005);
        $rpePatTasa = nlm_param($conexion, 'RPE_PATRONO_TASA', $fechaPago, 0.02);
        $rpeMin = $salMin * nlm_param($conexion, 'RPE_MIN_SALARIOS_MINIMOS', $fechaPago, 1.0) * $factor;
        $rpeMax = $salMin * nlm_param($conexion, 'RPE_TOPE_SALARIOS_MINIMOS', $fechaPago, 10.0) * $factor;
        $rpeBase = nlm_cap_base($baseNormal, $rpeMin, $rpeMax);

        $faovTrabTasa = nlm_param($conexion, 'FAOV_TRABAJADOR_TASA', $fechaPago, 0.01);
        $faovPatTasa = nlm_param($conexion, 'FAOV_PATRONO_TASA', $fechaPago, 0.02);
        $faovBase = $baseIntegral;

        $incesPatTasa = nlm_param($conexion, 'INCES_PATRONO_TASA', $fechaPago, 0.02);
        $incesBase = $baseNormal;

        $pensionesTasa = nlm_param($conexion, 'PENSIONES_PATRONO_TASA', $fechaPago, 0.09);
        $pensionesAplicarMin = nlm_param($conexion, 'PENSIONES_APLICAR_BASE_MINIMA', $fechaPago, 1.0) > 0.5;
        $pensionesMinUsd = nlm_param($conexion, 'PENSIONES_BASE_MINIMA_USD', $fechaPago, 0.0);
        $pensionesBaseMin = $pensionesAplicarMin ? ($pensionesMinUsd * (float)$tasaBcv * $factor) : 0.0;
        $pensionesBase = max($totalAsignaciones, $pensionesBaseMin);

        $items = [
            'B-001' => ['base'=>$ivssBasePeriodo, 'tasa'=>$ivssTrabTasa, 'monto'=>round($ivssBasePeriodo * $ivssTrabTasa, 2), 'meta'=>['motor'=>'legal_master','metodo'=>'IVSS semanal por lunes del periodo','base_mensual_tope'=>$ivssBaseMensual,'base_semanal'=>$ivssBaseSemanal,'lunes'=>$lunes,'tope_salarios_minimos'=>$ivssTope]],
            'B-002' => ['base'=>$rpeBase, 'tasa'=>$rpeTrabTasa, 'monto'=>round($rpeBase * $rpeTrabTasa, 2), 'meta'=>['motor'=>'legal_master','base_original'=>$baseNormal,'min_periodo'=>$rpeMin,'max_periodo'=>$rpeMax,'period_factor'=>$factor]],
            'B-003' => ['base'=>$faovBase, 'tasa'=>$faovTrabTasa, 'monto'=>round($faovBase * $faovTrabTasa, 2), 'meta'=>['motor'=>'legal_master','base'=>'salario_integral_parametrizado','base_normal'=>$baseNormal]],
            'P-001' => ['base'=>$ivssBasePeriodo, 'tasa'=>$ivssPatTasa, 'monto'=>round($ivssBasePeriodo * $ivssPatTasa, 2), 'meta'=>['motor'=>'legal_master','riesgo'=>'configurable','base_mensual_tope'=>$ivssBaseMensual,'base_semanal'=>$ivssBaseSemanal,'lunes'=>$lunes]],
            'P-002' => ['base'=>$rpeBase, 'tasa'=>$rpePatTasa, 'monto'=>round($rpeBase * $rpePatTasa, 2), 'meta'=>['motor'=>'legal_master','base_original'=>$baseNormal,'period_factor'=>$factor]],
            'P-003' => ['base'=>$faovBase, 'tasa'=>$faovPatTasa, 'monto'=>round($faovBase * $faovPatTasa, 2), 'meta'=>['motor'=>'legal_master','base'=>'salario_integral_parametrizado']],
            'P-004' => ['base'=>$incesBase, 'tasa'=>$incesPatTasa, 'monto'=>round($incesBase * $incesPatTasa, 2), 'meta'=>['motor'=>'legal_master','declaracion'=>'trimestral','base'=>'salario_normal']],
            'P-005' => ['base'=>$pensionesBase, 'tasa'=>$pensionesTasa, 'monto'=>round($pensionesBase * $pensionesTasa, 2), 'meta'=>['motor'=>'legal_master','base_pagos_reales'=>$totalAsignaciones,'base_minima_usd'=>$pensionesMinUsd,'base_minima_bs_periodo'=>$pensionesBaseMin,'period_factor'=>$factor]],
        ];
        return $items;
    }
}

if (!function_exists('nlm_snapshot_params')) {
    function nlm_snapshot_params($conexion, $fechaPago) {
        $fecha = nomina_date_to_iso($fechaPago);
        if ($fecha === '') { $fecha = date('Y-m-d'); }
        $fechaSql = mysqli_real_escape_string($conexion, $fecha);
        $sql = "SELECT codigo, nombre, categoria, valor_decimal, unidad, vigencia_desde, vigencia_hasta
                FROM nomina_parametros_legales p
                WHERE activo = 1
                  AND vigencia_desde <= '{$fechaSql}'
                  AND (vigencia_hasta IS NULL OR vigencia_hasta >= '{$fechaSql}')
                ORDER BY categoria, codigo, vigencia_desde DESC";
        $rows = [];
        $res = mysqli_query($conexion, $sql);
        if ($res) {
            while ($row = mysqli_fetch_assoc($res)) {
                if (!isset($rows[$row['codigo']])) { $rows[$row['codigo']] = $row; }
            }
        }
        return $rows;
    }
}

if (!function_exists('nlm_audit')) {
    function nlm_audit($conexion, $entidad, $entidadId, $accion, array $detalle = []) {
        if (!mysqli_query($conexion, "SHOW TABLES LIKE 'nomina_auditoria'")) { return false; }
        $entidadSql = mysqli_real_escape_string($conexion, $entidad);
        $accionSql = mysqli_real_escape_string($conexion, $accion);
        $usuario = isset($_SESSION['usuario']) ? (string)$_SESSION['usuario'] : '';
        $usuarioSql = mysqli_real_escape_string($conexion, $usuario);
        $ipSql = mysqli_real_escape_string($conexion, $_SERVER['REMOTE_ADDR'] ?? '');
        $jsonSql = mysqli_real_escape_string($conexion, json_encode($detalle, JSON_UNESCAPED_UNICODE));
        $entidadId = $entidadId === null ? 'NULL' : (int)$entidadId;
        return nlm_sql($conexion, "INSERT INTO nomina_auditoria (entidad, entidad_id, accion, usuario, detalle, ip)
                                    VALUES ('{$entidadSql}', {$entidadId}, '{$accionSql}', '{$usuarioSql}', '{$jsonSql}', '{$ipSql}')");
    }
}

if (!function_exists('nlm_estado_siguiente')) {
    function nlm_estado_siguiente($estadoActual) {
        $estadoActual = strtoupper((string)$estadoActual);
        $map = [
            'CALCULADA' => 'REVISADA',
            'REVISADA' => 'APROBADA',
            'APROBADA' => 'PAGADA',
            'PAGADA' => 'CERRADA',
        ];
        return $map[$estadoActual] ?? null;
    }
}

if (!function_exists('nlm_estado_accion_label')) {
    function nlm_estado_accion_label($estadoActual) {
        $estadoActual = strtoupper((string)$estadoActual);
        $map = [
            'CALCULADA' => 'Marcar como revisada',
            'REVISADA' => 'Aprobar corrida',
            'APROBADA' => 'Marcar como pagada',
            'PAGADA' => 'Cerrar período',
        ];
        return $map[$estadoActual] ?? '';
    }
}

if (!function_exists('nlm_estado_bloquea_periodo')) {
    function nlm_estado_bloquea_periodo($estado) {
        return in_array(strtoupper((string)$estado), ['REVISADA','APROBADA','PAGADA','CERRADA'], true);
    }
}

if (!function_exists('nlm_periodo_has_locked_corrida')) {
    function nlm_periodo_has_locked_corrida($conexion, $periodoId) {
        $periodoId = (int)$periodoId;
        if ($periodoId <= 0) return false;
        $res = mysqli_query($conexion, "SELECT COUNT(*) AS total FROM nomina_corridas WHERE periodo_id = {$periodoId} AND estado IN ('REVISADA','APROBADA','PAGADA','CERRADA')");
        $row = $res ? mysqli_fetch_assoc($res) : ['total'=>0];
        return (int)($row['total'] ?? 0) > 0;
    }
}



if (!function_exists('nlm_corrida_vigencia_periodo')) {
    /**
     * Devuelve el estado de vigencia operativa de una corrida dentro de su periodo.
     * Regla auditor v8.5: solo la corrida no anulada con mayor ID del periodo debe avanzar/cerrarse.
     * Esto evita cerrar por error una corrida de prueba anterior cuando luego se generó otra con novedades/vacaciones.
     */
    function nlm_corrida_vigencia_periodo($conexion, $corridaId) {
        $corridaId = (int)$corridaId;
        $out = [
            'ok' => false,
            'is_current' => false,
            'latest_id' => 0,
            'latest_estado' => '',
            'latest_fecha_calculo' => '',
            'periodo_id' => 0,
            'periodo_codigo' => '',
            'newer_count' => 0,
            'message' => 'Corrida no encontrada.'
        ];
        if ($corridaId <= 0) { return $out; }
        $res = mysqli_query($conexion, "SELECT nc.id, nc.periodo_id, nc.estado, nc.fecha_calculo, p.codigo AS periodo_codigo FROM nomina_corridas nc LEFT JOIN periodos_nomina p ON p.id = nc.periodo_id WHERE nc.id = {$corridaId} LIMIT 1");
        if (!$res || !mysqli_num_rows($res)) { return $out; }
        $row = mysqli_fetch_assoc($res);
        $periodoId = (int)($row['periodo_id'] ?? 0);
        $periodoCodigo = (string)($row['periodo_codigo'] ?? '');
        $out['ok'] = true;
        $out['periodo_id'] = $periodoId;
        $out['periodo_codigo'] = $periodoCodigo;
        if ($periodoId <= 0) {
            $out['message'] = 'La corrida no tiene periodo asociado.';
            return $out;
        }
        $latest = mysqli_query($conexion, "SELECT id, estado, fecha_calculo FROM nomina_corridas WHERE periodo_id = {$periodoId} AND estado <> 'ANULADA' ORDER BY id DESC LIMIT 1");
        if ($latest && mysqli_num_rows($latest)) {
            $lr = mysqli_fetch_assoc($latest);
            $out['latest_id'] = (int)($lr['id'] ?? 0);
            $out['latest_estado'] = (string)($lr['estado'] ?? '');
            $out['latest_fecha_calculo'] = (string)($lr['fecha_calculo'] ?? '');
        }
        $newer = mysqli_query($conexion, "SELECT COUNT(*) AS total FROM nomina_corridas WHERE periodo_id = {$periodoId} AND estado <> 'ANULADA' AND id > {$corridaId}");
        if ($newer) {
            $nr = mysqli_fetch_assoc($newer);
            $out['newer_count'] = (int)($nr['total'] ?? 0);
        }
        $out['is_current'] = ($out['latest_id'] === $corridaId);
        if ($out['is_current']) {
            $out['message'] = 'Corrida vigente de revisión para el periodo ' . $periodoCodigo . '.';
        } else {
            $out['message'] = 'Esta corrida fue superada por la corrida #' . $out['latest_id'] . ' del mismo periodo ' . $periodoCodigo . '. No debe avanzar ni cerrarse.';
        }
        return $out;
    }
}

if (!function_exists('nlm_corrida_puede_avanzar_por_vigencia')) {
    function nlm_corrida_puede_avanzar_por_vigencia($conexion, $corridaId) {
        $vig = nlm_corrida_vigencia_periodo($conexion, $corridaId);
        if (!($vig['ok'] ?? false)) { return ['ok'=>false, 'message'=>$vig['message'] ?? 'No se pudo validar vigencia de corrida.']; }
        if (!($vig['is_current'] ?? false)) { return ['ok'=>false, 'message'=>$vig['message'] ?? 'La corrida no es la vigente del periodo.']; }
        return ['ok'=>true, 'message'=>$vig['message'] ?? 'Corrida vigente.'];
    }
}

if (!function_exists('nlm_transition_corrida_estado')) {
    function nlm_transition_corrida_estado($conexion, $corridaId, $estadoNuevo, $motivo = '') {
        $corridaId = (int)$corridaId;
        $estadoNuevo = strtoupper(trim((string)$estadoNuevo));
        $motivo = trim((string)$motivo);
        if ($corridaId <= 0) {
            return ['ok'=>false, 'message'=>'Corrida inválida.'];
        }
        $res = mysqli_query($conexion, "SELECT * FROM nomina_corridas WHERE id = {$corridaId} LIMIT 1 FOR UPDATE");
        if (!$res || !mysqli_num_rows($res)) {
            return ['ok'=>false, 'message'=>'La corrida no existe.'];
        }
        $row = mysqli_fetch_assoc($res);
        $estadoActual = strtoupper((string)$row['estado']);
        $esperado = nlm_estado_siguiente($estadoActual);
        if ($esperado === null || $estadoNuevo !== $esperado) {
            return ['ok'=>false, 'message'=>'Transición no permitida: ' . $estadoActual . ' → ' . $estadoNuevo . '.'];
        }
        $motivoLen = function_exists('mb_strlen') ? mb_strlen($motivo, 'UTF-8') : strlen($motivo);
        if ($motivoLen < 10) {
            return ['ok'=>false, 'message'=>'Indica una observación/motivo claro de al menos 10 caracteres para avanzar la corrida a ' . $estadoNuevo . '.'];
        }

        // Blindaje v8.5: no permitir avanzar/cerrar una corrida superada por otra posterior del mismo periodo.
        // Esto evita cerrar por error una corrida calculada antes de copiar vacaciones u otras novedades.
        if (function_exists('nlm_corrida_puede_avanzar_por_vigencia')) {
            $vigOk = nlm_corrida_puede_avanzar_por_vigencia($conexion, $corridaId);
            if (!($vigOk['ok'] ?? false)) {
                return ['ok'=>false, 'message'=>$vigOk['message'] ?? 'Esta corrida no es la vigente del periodo.'];
            }
        }

        $usuario = isset($_SESSION['usuario']) ? (string)$_SESSION['usuario'] : 'sistema';
        $usuarioSql = mysqli_real_escape_string($conexion, $usuario);
        $motivoSql = mysqli_real_escape_string($conexion, $motivo);
        $ipSql = mysqli_real_escape_string($conexion, $_SERVER['REMOTE_ADDR'] ?? '');
        $sets = ["estado = '{$estadoNuevo}'", "estado_motivo = '" . $motivoSql . "'", "bloqueada = 1"];
        if ($estadoNuevo === 'REVISADA') { $sets[] = "revisada_at = NOW()"; $sets[] = "revisada_por = '{$usuarioSql}'"; }
        if ($estadoNuevo === 'APROBADA') { $sets[] = "aprobada_at = NOW()"; $sets[] = "aprobada_por = '{$usuarioSql}'"; }
        if ($estadoNuevo === 'PAGADA') { $sets[] = "pagada_at = NOW()"; $sets[] = "pagada_por = '{$usuarioSql}'"; }
        if ($estadoNuevo === 'CERRADA') { $sets[] = "cerrada_at = NOW()"; $sets[] = "cerrada_por = '{$usuarioSql}'"; }

        $ok = mysqli_query($conexion, "UPDATE nomina_corridas SET " . implode(', ', $sets) . " WHERE id = {$corridaId}");
        if (!$ok) {
            return ['ok'=>false, 'message'=>mysqli_error($conexion)];
        }
        mysqli_query($conexion, "INSERT INTO nomina_corrida_estado_historial (corrida_id, estado_anterior, estado_nuevo, usuario, motivo, ip) VALUES ({$corridaId}, '{$estadoActual}', '{$estadoNuevo}', '{$usuarioSql}', '{$motivoSql}', '{$ipSql}')");
        if ($estadoNuevo === 'CERRADA') {
            $periodoId = (int)($row['periodo_id'] ?? 0);
            if ($periodoId > 0) {
                mysqli_query($conexion, "UPDATE periodos_nomina SET estado = 'CERRADO' WHERE id = {$periodoId}");
            }
        }
        nlm_audit($conexion, 'nomina_corridas', $corridaId, 'CAMBIO_ESTADO_' . $estadoNuevo, ['estado_anterior'=>$estadoActual, 'estado_nuevo'=>$estadoNuevo, 'motivo'=>$motivo]);
        return ['ok'=>true, 'message'=>'Estado actualizado a ' . $estadoNuevo . '.'];
    }
}

if (!function_exists('nlm_current_user_name')) {
    function nlm_current_user_name() {
        if (isset($_SESSION['usuario']) && $_SESSION['usuario'] !== '') { return (string)$_SESSION['usuario']; }
        if (isset($_SESSION['username']) && $_SESSION['username'] !== '') { return (string)$_SESSION['username']; }
        return 'sistema';
    }
}

if (!function_exists('nlm_is_admin_user')) {
    function nlm_is_admin_user() {
        $u = strtolower(nlm_current_user_name());
        $rol = strtolower((string)($_SESSION['rol'] ?? $_SESSION['perfil'] ?? ''));
        return in_array($u, ['admin','administrador'], true) || in_array($rol, ['admin','administrador','rrhh_admin'], true);
    }
}

if (!function_exists('nlm_snapshot_concepts_for_corrida')) {
    function nlm_snapshot_concepts_for_corrida($conexion, $corridaId) {
        $corridaId = (int)$corridaId;
        if ($corridaId <= 0) { return false; }
        $hasRegla = nlm_column_exists($conexion, 'conceptos_nomina', 'regla_json');
        $reglaSelect = $hasRegla ? 'regla_json' : 'NULL AS regla_json';
        $sql = "SELECT id, codigo, nombre, tipo, incide_salario_normal, incide_base_cotizacion, modo_calculo, tasa, monto_fijo, moneda_monto_fijo, aplica_global, activo, {$reglaSelect} FROM conceptos_nomina";
        $res = mysqli_query($conexion, $sql);
        if (!$res) { return false; }
        while ($c = mysqli_fetch_assoc($res)) {
            $conceptoId = (int)($c['id'] ?? 0);
            $codigo = mysqli_real_escape_string($conexion, (string)($c['codigo'] ?? ''));
            $nombre = mysqli_real_escape_string($conexion, (string)($c['nombre'] ?? ''));
            $tipo = mysqli_real_escape_string($conexion, (string)($c['tipo'] ?? ''));
            $modo = mysqli_real_escape_string($conexion, (string)($c['modo_calculo'] ?? ''));
            $moneda = mysqli_real_escape_string($conexion, (string)($c['moneda_monto_fijo'] ?? 'VES'));
            $regla = $c['regla_json'] ?? null;
            $reglaSql = ($regla === null || $regla === '') ? 'NULL' : "'" . mysqli_real_escape_string($conexion, (string)$regla) . "'";
            $tasaSql = ($c['tasa'] === null || $c['tasa'] === '') ? 'NULL' : (string)(float)$c['tasa'];
            $montoSql = ($c['monto_fijo'] === null || $c['monto_fijo'] === '') ? 'NULL' : (string)(float)$c['monto_fijo'];
            $incideNormal = (int)($c['incide_salario_normal'] ?? 0);
            $incideBase = (int)($c['incide_base_cotizacion'] ?? 0);
            $aplicaGlobal = (int)($c['aplica_global'] ?? 0);
            $activo = (int)($c['activo'] ?? 1);
            $existsSnap = mysqli_query($conexion, "SELECT id FROM nomina_corrida_conceptos_snapshot WHERE corrida_id = {$corridaId} AND concepto_id = {$conceptoId} LIMIT 1");
            if ($existsSnap && mysqli_num_rows($existsSnap) > 0) {
                $rowSnap = mysqli_fetch_assoc($existsSnap);
                $snapId = (int)($rowSnap['id'] ?? 0);
                mysqli_query($conexion, "UPDATE nomina_corrida_conceptos_snapshot SET
                    codigo='{$codigo}', nombre='{$nombre}', tipo='{$tipo}', incide_salario_normal={$incideNormal}, incide_base_cotizacion={$incideBase}, modo_calculo='{$modo}', tasa={$tasaSql}, monto_fijo={$montoSql}, moneda_monto_fijo='{$moneda}', aplica_global={$aplicaGlobal}, activo={$activo}, regla_json={$reglaSql}
                    WHERE id={$snapId} LIMIT 1");
            } else {
                mysqli_query($conexion, "INSERT INTO nomina_corrida_conceptos_snapshot
                    (corrida_id, concepto_id, codigo, nombre, tipo, incide_salario_normal, incide_base_cotizacion, modo_calculo, tasa, monto_fijo, moneda_monto_fijo, aplica_global, activo, regla_json)
                    VALUES ({$corridaId}, {$conceptoId}, '{$codigo}', '{$nombre}', '{$tipo}', {$incideNormal}, {$incideBase}, '{$modo}', {$tasaSql}, {$montoSql}, '{$moneda}', {$aplicaGlobal}, {$activo}, {$reglaSql})");
            }
        }
        nlm_audit($conexion, 'nomina_corridas', $corridaId, 'SNAPSHOT_CONCEPTOS', ['total'=>'conceptos congelados en tabla snapshot']);
        return true;
    }
}

if (!function_exists('nlm_salary_snapshot_context')) {
    function nlm_salary_snapshot_context(array $persona, $tasaBcv) {
        $salPropio = (isset($persona['salario_base']) && $persona['salario_base'] !== null && $persona['salario_base'] !== '');
        $salCargo = (!$salPropio && isset($persona['salario_cargo']) && $persona['salario_cargo'] !== null && $persona['salario_cargo'] !== '');
        if ($salPropio) {
            $origen = 'FICHA';
            $monto = (float)$persona['salario_base'];
            $moneda = strtoupper((string)($persona['moneda_salario'] ?? 'VES'));
        } elseif ($salCargo) {
            $origen = 'CARGO';
            $monto = (float)$persona['salario_cargo'];
            $moneda = strtoupper((string)($persona['moneda_cargo'] ?? 'VES'));
        } else {
            $origen = 'INCOMPLETO';
            $monto = 0.0;
            $moneda = 'VES';
        }
        $salarioBs = ($moneda === 'USD') ? ($monto * (float)$tasaBcv) : $monto;
        return [
            'origen' => $origen,
            'moneda' => $moneda,
            'monto_original' => $monto,
            'salario_mensual_bs' => $salarioBs,
            'salario_diario_bs' => $salarioBs / 30.0,
            'salario_hora_bs' => ($salarioBs / 30.0) / 8.0,
        ];
    }
}

if (!function_exists('nlm_snapshot_worker_for_corrida')) {
    function nlm_snapshot_worker_for_corrida($conexion, $corridaId, array $persona, $tasaBcv) {
        $corridaId = (int)$corridaId;
        $personaId = (int)($persona['id'] ?? 0);
        if ($corridaId <= 0 || $personaId <= 0) { return false; }
        $salary = nlm_salary_snapshot_context($persona, $tasaBcv);
        $nombre = mysqli_real_escape_string($conexion, (string)($persona['nombre'] ?? ''));
        $apellido = mysqli_real_escape_string($conexion, (string)($persona['apellido'] ?? ''));
        $cedula = mysqli_real_escape_string($conexion, (string)($persona['cedula'] ?? ''));
        $cargo = mysqli_real_escape_string($conexion, (string)($persona['cargo'] ?? ''));
        $fechaIngreso = !empty($persona['fecha_ingreso']) ? "'" . mysqli_real_escape_string($conexion, (string)$persona['fecha_ingreso']) . "'" : 'NULL';
        $estatus = mysqli_real_escape_string($conexion, (string)($persona['estatus_laboral'] ?? 'ACTIVO'));
        $origen = mysqli_real_escape_string($conexion, $salary['origen']);
        $moneda = mysqli_real_escape_string($conexion, $salary['moneda']);
        $bonoUsd = (float)($persona['bono_extra_usd_mensual'] ?? 0);
        $json = mysqli_real_escape_string($conexion, json_encode($persona, JSON_UNESCAPED_UNICODE));
        $idCargo = isset($persona['id_cargo']) && $persona['id_cargo'] !== '' ? (int)$persona['id_cargo'] : 'NULL';
        $montoOriginal = (float)$salary['monto_original'];
        $salarioMensual = (float)$salary['salario_mensual_bs'];
        $salarioDiario = (float)$salary['salario_diario_bs'];
        $salarioHora = (float)$salary['salario_hora_bs'];
        $tasaBcvNum = (float)$tasaBcv;
        $existsSnap = mysqli_query($conexion, "SELECT id FROM nomina_corrida_trabajador_snapshot WHERE corrida_id = {$corridaId} AND persona_id = {$personaId} LIMIT 1");
        if ($existsSnap && mysqli_num_rows($existsSnap) > 0) {
            $rowSnap = mysqli_fetch_assoc($existsSnap);
            $snapId = (int)($rowSnap['id'] ?? 0);
            $sql = "UPDATE nomina_corrida_trabajador_snapshot SET
                nombre='{$nombre}', apellido='{$apellido}', cedula='{$cedula}', cargo='{$cargo}', id_cargo={$idCargo}, fecha_ingreso={$fechaIngreso}, estatus_laboral='{$estatus}', salario_origen='{$origen}', salario_moneda='{$moneda}', salario_monto_original={$montoOriginal}, salario_mensual_bs={$salarioMensual}, salario_diario_bs={$salarioDiario}, salario_hora_bs={$salarioHora}, bono_extra_usd_mensual={$bonoUsd}, tasa_bcv_usada={$tasaBcvNum}, snapshot_json='{$json}'
                WHERE id={$snapId} LIMIT 1";
        } else {
            $sql = "INSERT INTO nomina_corrida_trabajador_snapshot
                (corrida_id, persona_id, nombre, apellido, cedula, cargo, id_cargo, fecha_ingreso, estatus_laboral, salario_origen, salario_moneda, salario_monto_original, salario_mensual_bs, salario_diario_bs, salario_hora_bs, bono_extra_usd_mensual, tasa_bcv_usada, snapshot_json)
                VALUES ({$corridaId}, {$personaId}, '{$nombre}', '{$apellido}', '{$cedula}', '{$cargo}', {$idCargo}, {$fechaIngreso}, '{$estatus}', '{$origen}', '{$moneda}', {$montoOriginal}, {$salarioMensual}, {$salarioDiario}, {$salarioHora}, {$bonoUsd}, {$tasaBcvNum}, '{$json}')";
        }
        return mysqli_query($conexion, $sql) !== false;
    }
}

if (!function_exists('nlm_corrida_is_locked')) {
    function nlm_corrida_is_locked($conexion, $corridaId) {
        $corridaId = (int)$corridaId;
        if ($corridaId <= 0) { return true; }
        $res = mysqli_query($conexion, "SELECT estado, bloqueada FROM nomina_corridas WHERE id = {$corridaId} LIMIT 1");
        $row = $res ? mysqli_fetch_assoc($res) : null;
        if (!$row) { return true; }
        return ((int)($row['bloqueada'] ?? 0) === 1) || in_array(strtoupper((string)$row['estado']), ['REVISADA','APROBADA','PAGADA','CERRADA','ANULADA'], true);
    }
}

if (!function_exists('nlm_corrida_ajustes')) {
    function nlm_corrida_ajustes($conexion, $corridaId) {
        $corridaId = (int)$corridaId;
        $rows = [];
        if ($corridaId <= 0) { return $rows; }
        $sql = "SELECT a.*, p.nombre, p.apellido, p.cedula, COALESCE(cs.codigo, c.codigo) AS codigo, COALESCE(cs.nombre, c.nombre) AS concepto_nombre
                FROM nomina_corrida_ajustes a
                LEFT JOIN personas p ON p.id = a.persona_id
                LEFT JOIN conceptos_nomina c ON c.id = a.concepto_id
                LEFT JOIN nomina_corrida_conceptos_snapshot cs ON cs.corrida_id = a.corrida_id AND cs.concepto_id = a.concepto_id
                WHERE a.corrida_id = {$corridaId}
                ORDER BY a.id DESC";
        $res = mysqli_query($conexion, $sql);
        if ($res) { while ($row = mysqli_fetch_assoc($res)) { $rows[] = $row; } }
        return $rows;
    }
}

if (!function_exists('nlm_registrar_ajuste_reverso')) {
    function nlm_registrar_ajuste_reverso($conexion, $corridaId, $tipo, $personaId, $conceptoId, $monto, $cantidad, $motivo, $referencia = '') {
        $corridaId = (int)$corridaId;
        $tipo = strtoupper(trim((string)$tipo));
        if (!in_array($tipo, ['AJUSTE','REVERSO'], true)) { return ['ok'=>false, 'message'=>'Tipo inválido.']; }
        $personaId = (int)$personaId;
        $conceptoId = (int)$conceptoId;
        $monto = (float)$monto;
        $cantidad = ($cantidad === null || $cantidad === '') ? null : (float)$cantidad;
        $motivo = trim((string)$motivo);
        $referencia = trim((string)$referencia);
        if ($corridaId <= 0) { return ['ok'=>false, 'message'=>'Corrida inválida.']; }
        if ($motivo === '' || mb_strlen($motivo) < 10) { return ['ok'=>false, 'message'=>'Indica un motivo claro de al menos 10 caracteres.']; }
        if ($monto == 0.0 && $tipo === 'AJUSTE') { return ['ok'=>false, 'message'=>'El monto del ajuste no puede ser cero.']; }
        $res = mysqli_query($conexion, "SELECT nc.*, p.id AS periodo_id FROM nomina_corridas nc INNER JOIN periodos_nomina p ON p.id = nc.periodo_id WHERE nc.id = {$corridaId} LIMIT 1");
        $corrida = $res ? mysqli_fetch_assoc($res) : null;
        if (!$corrida) { return ['ok'=>false, 'message'=>'La corrida no existe.']; }
        $estado = strtoupper((string)($corrida['estado'] ?? ''));
        if (!in_array($estado, ['APROBADA','PAGADA','CERRADA'], true)) {
            return ['ok'=>false, 'message'=>'Solo se registran ajustes/reversos formales sobre corridas aprobadas, pagadas o cerradas.'];
        }
        $personaSql = $personaId > 0 ? (string)$personaId : 'NULL';
        $conceptoSql = $conceptoId > 0 ? (string)$conceptoId : 'NULL';
        $cantidadSql = $cantidad === null ? 'NULL' : (string)$cantidad;
        $motivoSql = mysqli_real_escape_string($conexion, $motivo);
        $referenciaSql = mysqli_real_escape_string($conexion, $referencia);
        $usuarioSql = mysqli_real_escape_string($conexion, nlm_current_user_name());
        $ipSql = mysqli_real_escape_string($conexion, $_SERVER['REMOTE_ADDR'] ?? '');
        $periodoId = (int)($corrida['periodo_id'] ?? 0);
        $signo = $monto < 0 ? 'DEBITO' : 'CREDITO';
        if ($tipo === 'REVERSO') { $signo = 'REVERSO'; }
        $ok = mysqli_query($conexion, "INSERT INTO nomina_corrida_ajustes
            (corrida_id, periodo_id, persona_id, concepto_id, tipo, signo, monto, cantidad, motivo, referencia, usuario, ip)
            VALUES ({$corridaId}, {$periodoId}, {$personaSql}, {$conceptoSql}, '{$tipo}', '{$signo}', {$monto}, {$cantidadSql}, '{$motivoSql}', '{$referenciaSql}', '{$usuarioSql}', '{$ipSql}')");
        if (!$ok) { return ['ok'=>false, 'message'=>mysqli_error($conexion)]; }
        $ajusteId = (int)mysqli_insert_id($conexion);
        nlm_audit($conexion, 'nomina_corrida_ajustes', $ajusteId, 'REGISTRO_' . $tipo, ['corrida_id'=>$corridaId, 'persona_id'=>$personaId, 'concepto_id'=>$conceptoId, 'monto'=>$monto, 'motivo'=>$motivo, 'referencia'=>$referencia]);
        return ['ok'=>true, 'message'=>$tipo . ' registrado correctamente sin alterar la corrida original.', 'ajuste_id'=>$ajusteId];
    }
}

?>
