<?php
/**
 * Funciones utilitarias para sincronización BD Local ↔ BD Nube
 */
if (!defined('BASEPATH')) {
    die('Acceso directo no permitido');
}

// -----------------------------------------------
// COMUNICACIÓN CON API NUBE
// -----------------------------------------------

/**
 * Llama al API de la nube
 *
 * @param string $url      URL del API nube
 * @param string $tipo     Tipo de operación (syncReceive, syncSend)
 * @param array  $payload  Datos a enviar
 * @param int    $timeout  Timeout en segundos
 * @return array ['success' => bool, 'data' => mixed, 'message' => string]
 */
function callCloudApi($url, $tipo, $payload, $timeout = 30) {
    $payload['tipo'] = $tipo;

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Accept: application/json',
        ],
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($error) {
        return [
            'success' => false,
            'data'    => null,
            'message' => "Error de conexión: $error",
        ];
    }

    if ($httpCode !== 200) {
        return [
            'success' => false,
            'data'    => null,
            'message' => "HTTP $httpCode: $response",
        ];
    }

    $decoded = json_decode($response, true);
    if ($decoded === null) {
        return [
            'success' => false,
            'data'    => null,
            'message' => "Respuesta inválida del servidor nube",
        ];
    }

    return $decoded;
}

// -----------------------------------------------
// COLUMNAS DE TABLA
// -----------------------------------------------

/**
 * Obtiene las columnas de una tabla excluyendo las indicadas
 *
 * @param mysqli $conn
 * @param string $table
 * @param array  $excludeCols  Columnas a excluir (ej: ['password'])
 * @return array Lista de nombres de columna
 */
function getTableColumns($conn, $table, $excludeCols = []) {
    $columns = [];
    $result = $conn->query("SHOW COLUMNS FROM `$table`");
    if (!$result) return $columns;

    while ($row = $result->fetch_assoc()) {
        $col = $row['Field'];
        // Excluir columnas de cloud (no existen en local) y las personalizadas
        if (in_array($col, $excludeCols)) continue;
        $columns[] = $col;
    }
    return $columns;
}

// -----------------------------------------------
// QUERIES DE UPSERT
// -----------------------------------------------

/**
 * Construye un INSERT ... ON DUPLICATE KEY UPDATE para la nube
 * La clave de dedup es (sucursal, id_origen)
 *
 * @param string $table
 * @param array  $columns    Columnas del registro (sin sucursal/id_origen)
 * @param string $pk         Nombre de la PK
 * @return string Query SQL con placeholders ?
 */
function buildCloudUpsertQuery($table, $columns, $pk) {
    // Columnas destino: todas las del registro + sucursal + id_origen
    $destCols = array_merge($columns, ['sucursal', 'id_origen']);
    // Quitar la PK del registro original (la nube genera su propio ID)
    $destCols = array_filter($destCols, function($c) use ($pk) {
        return $c !== $pk;
    });
    $destCols = array_values($destCols);

    $colList = implode(', ', array_map(function($c) { return "`$c`"; }, $destCols));
    $placeholders = implode(', ', array_fill(0, count($destCols), '?'));

    // ON DUPLICATE KEY UPDATE: actualizar todo excepto sucursal e id_origen
    $updateParts = [];
    foreach ($destCols as $col) {
        if ($col === 'sucursal' || $col === 'id_origen') continue;
        $updateParts[] = "`$col` = VALUES(`$col`)";
    }
    $updateClause = implode(', ', $updateParts);

    $sql = "INSERT INTO `$table` ($colList) VALUES ($placeholders)
            ON DUPLICATE KEY UPDATE $updateClause";

    return ['sql' => $sql, 'columns' => $destCols];
}

/**
 * Construye un INSERT ... ON DUPLICATE KEY UPDATE para inserción local (pull)
 * Actualiza por PK si el registro ya existe
 *
 * @param string $table
 * @param array  $columns  Columnas a insertar
 * @param string $pk       Nombre de la PK
 * @return string
 */
function buildLocalUpsertQuery($table, $columns, $pk) {
    $colList = implode(', ', array_map(function($c) { return "`$c`"; }, $columns));
    $placeholders = implode(', ', array_fill(0, count($columns), '?'));

    $updateParts = [];
    foreach ($columns as $col) {
        if ($col === $pk) continue;
        $updateParts[] = "`$col` = VALUES(`$col`)";
    }
    $updateClause = implode(', ', $updateParts);

    return "INSERT INTO `$table` ($colList) VALUES ($placeholders)
            ON DUPLICATE KEY UPDATE $updateClause";
}

// -----------------------------------------------
// TIMESTAMPS Y CONTROL DE SYNC
// -----------------------------------------------

/**
 * Lee el último timestamp de sync para una tabla/sucursal/dirección
 *
 * @param mysqli $conn
 * @param string $table
 * @param string $sucursal
 * @param string $direction  'push' o 'pull'
 * @return string|null  Datetime string o null si nunca se ha sincronizado
 */
function getLastSyncTimestamp($conn, $table, $sucursal, $direction) {
    $col = ($direction === 'push') ? 'last_push_at' : 'last_pull_at';
    $stmt = $conn->prepare("SELECT $col FROM sync_control WHERE tabla = ? AND sucursal = ?");
    $stmt->bind_param('ss', $table, $sucursal);
    $stmt->execute();
    $result = $stmt->get_result();
    $row = $result->fetch_assoc();
    $stmt->close();

    return $row ? $row[$col] : null;
}

/**
 * Actualiza el timestamp de sync para una tabla/sucursal/dirección
 *
 * @param mysqli $conn
 * @param string $table
 * @param string $sucursal
 * @param string $direction   'push' o 'pull'
 * @param string $timestamp   Datetime(6) string
 */
function updateSyncTimestamp($conn, $table, $sucursal, $direction, $timestamp) {
    $col = ($direction === 'push') ? 'last_push_at' : 'last_pull_at';

    $sql = "INSERT INTO sync_control (tabla, sucursal, $col)
            VALUES (?, ?, ?)
            ON DUPLICATE KEY UPDATE $col = VALUES($col)";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param('sss', $table, $sucursal, $timestamp);
    $stmt->execute();
    $stmt->close();
}

// -----------------------------------------------
// LOGGING
// -----------------------------------------------

/**
 * Registra una entrada en sync_log
 *
 * @param mysqli $conn
 * @param array  $data  Campos del log
 * @return int  ID del log creado
 */
function logSync($conn, $data) {
    $defaults = [
        'tabla'                  => '',
        'sucursal'               => '',
        'direccion'              => 'push',
        'registros_enviados'     => 0,
        'registros_recibidos'    => 0,
        'registros_conflictos'   => 0,
        'ultimo_sync_timestamp'  => null,
        'estado'                 => 'iniciado',
        'error_mensaje'          => null,
        'duracion_ms'            => 0,
    ];
    $d = array_merge($defaults, $data);

    $sql = "INSERT INTO sync_log
            (tabla, sucursal, direccion, registros_enviados, registros_recibidos,
             registros_conflictos, ultimo_sync_timestamp, estado, error_mensaje, duracion_ms)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param(
        'sssiiisssi',
        $d['tabla'], $d['sucursal'], $d['direccion'],
        $d['registros_enviados'], $d['registros_recibidos'], $d['registros_conflictos'],
        $d['ultimo_sync_timestamp'], $d['estado'], $d['error_mensaje'], $d['duracion_ms']
    );
    $stmt->execute();
    $id = $stmt->insert_id;
    $stmt->close();

    // Purgar logs antiguos si superan 3000 registros
    purgeSyncLog($conn, 3000);

    return $id;
}

/**
 * Elimina los registros más antiguos de sync_log si supera el límite
 */
function purgeSyncLog($conn, $maxRows = 3000) {
    $result = $conn->query("SELECT COUNT(*) as total FROM sync_log");
    $total = (int)$result->fetch_assoc()['total'];

    if ($total > $maxRows) {
        $toDelete = $total - $maxRows;
        $conn->query("DELETE FROM sync_log ORDER BY created_at ASC LIMIT $toDelete");
    }
}

/**
 * Actualiza un registro de sync_log existente
 *
 * @param mysqli $conn
 * @param int    $logId
 * @param array  $data  Campos a actualizar
 */
function updateSyncLog($conn, $logId, $data) {
    $sets = [];
    $params = [];
    $types = '';

    foreach ($data as $key => $value) {
        $sets[] = "$key = ?";
        $params[] = $value;
        if (is_int($value)) {
            $types .= 'i';
        } else {
            $types .= 's';
        }
    }

    $types .= 'i';
    $params[] = $logId;

    $sql = "UPDATE sync_log SET " . implode(', ', $sets) . " WHERE id = ?";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param($types, ...$params);
    $stmt->execute();
    $stmt->close();
}

// -----------------------------------------------
// MAPEO DE IDs (para pull - traduce IDs remotos a locales)
// -----------------------------------------------

/**
 * Busca el ID local correspondiente a un ID remoto
 *
 * @param mysqli $conn
 * @param string $table
 * @param string $sucursalOrigen
 * @param int    $idRemoto
 * @return int|null  ID local o null si no existe
 */
function lookupLocalId($conn, $table, $sucursalOrigen, $idRemoto) {
    $stmt = $conn->prepare(
        "SELECT id_local FROM sync_id_map WHERE tabla = ? AND sucursal_origen = ? AND id_remoto = ?"
    );
    $stmt->bind_param('ssi', $table, $sucursalOrigen, $idRemoto);
    $stmt->execute();
    $result = $stmt->get_result();
    $row = $result->fetch_assoc();
    $stmt->close();

    return $row ? (int)$row['id_local'] : null;
}

/**
 * Guarda o actualiza el mapeo de un ID remoto a local
 *
 * @param mysqli $conn
 * @param string $table
 * @param string $sucursalOrigen
 * @param int    $idRemoto
 * @param int    $idLocal
 */
function saveIdMapping($conn, $table, $sucursalOrigen, $idRemoto, $idLocal) {
    $sql = "INSERT INTO sync_id_map (tabla, sucursal_origen, id_remoto, id_local)
            VALUES (?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE id_local = VALUES(id_local)";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param('ssii', $table, $sucursalOrigen, $idRemoto, $idLocal);
    $stmt->execute();
    $stmt->close();
}

/**
 * Busca el ID local por tabla e id_remoto para cualquier sucursal
 *
 * @param mysqli $conn
 * @param string $table
 * @param int    $idRemoto
 * @param string $sucursalOrigen
 * @return int|null
 */
function lookupLocalIdBySucursal($conn, $table, $idRemoto, $sucursalOrigen) {
    return lookupLocalId($conn, $table, $sucursalOrigen, $idRemoto);
}

// -----------------------------------------------
// FK MAPPING (traduce FKs en registros entrantes)
// -----------------------------------------------

/**
 * Mapea las foreign keys de un registro remoto a IDs locales
 *
 * @param mysqli $conn
 * @param array  $record          Registro con datos remotos
 * @param array  $fkDefinitions   ['columna_fk' => 'tabla_referenciada', ...]
 * @param string $sucursalOrigen  Sucursal de donde viene el registro
 * @return array  Registro con FKs mapeadas a IDs locales
 */
function mapForeignKeys($conn, $record, $fkDefinitions, $sucursalOrigen) {
    foreach ($fkDefinitions as $fkCol => $refTable) {
        if (!isset($record[$fkCol]) || $record[$fkCol] === null || $record[$fkCol] === '') {
            continue;
        }

        $remoteId = (int)$record[$fkCol];
        $localId = lookupLocalId($conn, $refTable, $sucursalOrigen, $remoteId);

        if ($localId !== null) {
            $record[$fkCol] = $localId;
        }
        // Si no se encuentra el mapeo, mantiene el ID original
        // Esto puede causar errores de FK pero se loguea como conflicto
    }

    return $record;
}

// -----------------------------------------------
// LECTURA DE REGISTROS MODIFICADOS
// -----------------------------------------------

/**
 * Obtiene registros modificados después de un timestamp dado, en lotes
 *
 * @param mysqli $conn
 * @param string $table
 * @param string $pk
 * @param string $timestampCol
 * @param string|null $since     Datetime o null para todos los registros
 * @param int    $batchSize
 * @param int    $offset
 * @param array  $excludeCols
 * @return array ['records' => [...], 'has_more' => bool, 'max_timestamp' => string]
 */
function getModifiedRecords($conn, $table, $pk, $timestampCol, $since, $batchSize, $offset = 0, $excludeCols = []) {
    $columns = getTableColumns($conn, $table, $excludeCols);

    if (empty($columns)) {
        return ['records' => [], 'has_more' => false, 'max_timestamp' => null];
    }

    $colList = implode(', ', array_map(function($c) { return "`$c`"; }, $columns));

    if ($since) {
        $sql = "SELECT $colList FROM `$table` WHERE `$timestampCol` > ? ORDER BY `$timestampCol` ASC LIMIT ? OFFSET ?";
        $stmt = $conn->prepare($sql);
        $stmt->bind_param('sii', $since, $batchSize, $offset);
    } else {
        $sql = "SELECT $colList FROM `$table` ORDER BY `$timestampCol` ASC LIMIT ? OFFSET ?";
        $stmt = $conn->prepare($sql);
        $stmt->bind_param('ii', $batchSize, $offset);
    }

    $stmt->execute();
    $result = $stmt->get_result();
    $records = [];
    $maxTimestamp = null;

    while ($row = $result->fetch_assoc()) {
        $records[] = $row;
        if (isset($row[$timestampCol])) {
            $maxTimestamp = $row[$timestampCol];
        }
    }
    $stmt->close();

    // Verificar si hay más registros
    $hasMore = (count($records) === $batchSize);

    return [
        'records'       => $records,
        'has_more'      => $hasMore,
        'max_timestamp' => $maxTimestamp,
    ];
}

/**
 * Cuenta registros pendientes de sync
 *
 * @param mysqli $conn
 * @param string $table
 * @param string $timestampCol
 * @param string|null $since
 * @return int
 */
function countPendingRecords($conn, $table, $timestampCol, $since) {
    if ($since) {
        $sql = "SELECT COUNT(*) as total FROM `$table` WHERE `$timestampCol` > ?";
        $stmt = $conn->prepare($sql);
        $stmt->bind_param('s', $since);
    } else {
        $sql = "SELECT COUNT(*) as total FROM `$table`";
        $stmt = $conn->prepare($sql);
    }

    $stmt->execute();
    $result = $stmt->get_result();
    $row = $result->fetch_assoc();
    $stmt->close();

    return (int)$row['total'];
}

// -----------------------------------------------
// RESOLUCIÓN DE CONFLICTOS
// -----------------------------------------------

/**
 * Resuelve conflicto last-write-wins
 * Si timestamps iguales, sucursal con menor nombre alfabético gana
 *
 * @param string $localTimestamp
 * @param string $remoteTimestamp
 * @param string $localSucursal
 * @param string $remoteSucursal
 * @return string  'local' o 'remote'
 */
function resolveConflict($localTimestamp, $remoteTimestamp, $localSucursal, $remoteSucursal) {
    if ($remoteTimestamp > $localTimestamp) {
        return 'remote';
    }
    if ($remoteTimestamp < $localTimestamp) {
        return 'local';
    }
    // Timestamps iguales: menor nombre alfabético gana
    return ($remoteSucursal < $localSucursal) ? 'remote' : 'local';
}
