#!/usr/bin/env python3
"""
Script para replicar tablas de gps_reportes de un MySQL origen a un MySQL destino.
- Crea tablas nuevas sin DROP/TRUNCATE.
- Copia datos en lotes con estimación de tiempo.
- Usa transacciones cortas y READ COMMITTED para minimizar locks.
- Logging con timestamps y flush para debug en caliente.
- Mantiene registro de progreso en la tabla 'migracion' en el servidor de origen.
"""
import sys
import time
import logging
import mysql.connector
from mysql.connector import errorcode

# Configuración de conexiones con credenciales proporcionadas
SOURCE_CONFIG = {
    'host': '10.2.12.226',
    'user': 'gps',
    'password': 'q1w2e3r4',
    'database': 'gps_reportes',
    'charset': 'utf8mb4',
    'use_pure': True,
}
DEST_CONFIG = {
    'host': '10.2.12.220',
    'user': 'gps',
    'password': 'q1w2e3r4',
    'database': 'gps_reportes',
    'charset': 'utf8mb4',
    'use_pure': True,
}

BATCH_SIZE = 1000  # Número de filas a copiar por lote
LOG_FORMAT = '%(asctime)s %(levelname)s: %(message)s'
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, stream=sys.stdout)
logger = logging.getLogger('replicator')

MIGRATION_TABLE_NAME = 'migracion' # Nombre de la tabla de seguimiento de progreso

# Plantilla de la sentencia CREATE TABLE para las tablas LOG_
# Se usa un placeholder {table_name} para insertar el nombre de la tabla dinámicamente.
CREATE_TABLE_TEMPLATE = """
CREATE TABLE `{table_name}` (
    `IdReporte` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `Reporte_IdDisp` varchar(30) DEFAULT NULL,
    `Reporte_FechaGPS` datetime DEFAULT NULL,
    `Reporte_FechaServidor` datetime DEFAULT NULL,
    `Reporte_Velocidad` decimal(18,2) DEFAULT NULL,
    `Reporte_Rumbo` int(11) DEFAULT NULL,
    `Reporte_Latitud` varchar(50) DEFAULT NULL,
    `Reporte_Longitud` varchar(50) DEFAULT NULL,
    `Reporte_Evento` varchar(2) DEFAULT NULL,
    `Reporte_EventoDescripcion` varchar(100) DEFAULT NULL,
    `Reporte_Entradas` varchar(20) DEFAULT NULL,
    `Reporte_Edad` varchar(4) DEFAULT NULL,
    `Reporte_Completo` varchar(1000) DEFAULT NULL,
    `Reporte_Puerto` varchar(45) DEFAULT NULL,
    `Reporte_PC` varchar(45) DEFAULT NULL,
    `Reporte_AlertaProcesada` int(11) DEFAULT NULL,
    `Reporte_NroMensaje` varchar(45) DEFAULT NULL,
    `Reporte_Val1` varchar(45) DEFAULT NULL,
    `Reporte_Val2` varchar(45) DEFAULT NULL,
    `Reporte_Val3` varchar(45) DEFAULT NULL,
    `Reporte_Val4` varchar(300) DEFAULT NULL,
    `Reporte_RUS` int(11) DEFAULT NULL,
    `Reporte_Direccion` varchar(400) DEFAULT NULL,
    `Reporte_Ignicion` varchar(2) DEFAULT NULL,
    `Reporte_Mileage` decimal(10,1) DEFAULT NULL,
    PRIMARY KEY (`IdReporte`),
    UNIQUE KEY `id_disp` (`Reporte_IdDisp`),
    UNIQUE KEY `Reporte_NroMensaje_Reporte_FechaGPS` (`Reporte_NroMensaje`,`Reporte_FechaGPS`),
    KEY `fecha1` (`Reporte_FechaServidor`),
    KEY `fecha2` (`Reporte_FechaGPS`),
    KEY `puerto` (`Reporte_Puerto`),
    KEY `rus` (`Reporte_RUS`),
    KEY `ignicion` (`Reporte_Ignicion`),
    KEY `idxDisp_fecha` (`Reporte_IdDisp','Reporte_FechaGPS')
) ENGINE=InnoDB;
"""

def create_table_if_not_exists_on_dest(dest_conn, table_name):
    """
    Verifica si una tabla existe en el destino y, si no, la crea usando la plantilla.
    """
    cursor = dest_conn.cursor()
    # Usamos INFORMATION_SCHEMA para una verificación más robusta
    cursor.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = %s",
                   (DEST_CONFIG['database'], table_name))
    exists = cursor.fetchone()[0] > 0
    cursor.close()

    if not exists:
        logger.info(f"Tabla '{table_name}' no existe en el destino. Creándola...")
        
        # Construir la sentencia CREATE TABLE usando la plantilla
        create_sql = CREATE_TABLE_TEMPLATE.format(table_name=table_name)
        
        dest_cursor_create = dest_conn.cursor()
        try:
            dest_cursor_create.execute(create_sql)
            dest_conn.commit()
            logger.info(f"Tabla '{table_name}' creada exitosamente en el destino.")
        except mysql.connector.Error as err:
            logger.error(f"Error al crear la tabla '{table_name}' en el destino: {err}")
            # Re-lanza la excepción para detener el script si la creación falla
            raise
        finally:
            dest_cursor_create.close()
    else:
        logger.info(f"Tabla '{table_name}' ya existe en el destino. Saltando la creación.")

def table_exists(conn, table):
    """
    Verifica si una tabla existe en la base de datos de una conexión dada.
    """
    cursor = conn.cursor()
    sql_query = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = %s"
    logger.debug(f"Ejecutando SQL: {sql_query} con params: {(conn.database, table)}")
    cursor.execute(sql_query, (conn.database, table))
    exists = cursor.fetchone()[0] > 0
    cursor.close()
    return exists

def create_migration_table_if_not_exists(conn):
    """
    Crea la tabla 'migracion' en la base de datos de origen si no existe.
    La columna 'tablamigrada' se establece como INT para ser un contador.
    """
    create_sql = f"""
    CREATE TABLE IF NOT EXISTS `{MIGRATION_TABLE_NAME}` (
        `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
        `tabla` VARCHAR(64) COLLATE utf8_unicode_ci NOT NULL,
        `fecha` DATETIME DEFAULT CURRENT_TIMESTAMP,
        `ultidreporte` BIGINT(20) DEFAULT NULL,
        `fechaupdate` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        `tablamigrada` INT(11) DEFAULT 0,
        PRIMARY KEY (`id`),
        UNIQUE KEY `uq_tabla` (`tabla`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    """
    cursor = conn.cursor()
    try:
        logger.debug(f"Ejecutando SQL: {create_sql}")
        cursor.execute(create_sql) 
        conn.commit()
        logger.info(f"Tabla '{MIGRATION_TABLE_NAME}' asegurada en el origen.")
    except mysql.connector.Error as err:
        logger.error(f"Error al crear/verificar tabla '{MIGRATION_TABLE_NAME}': {err}")
        raise
    finally:
        cursor.close()

def _check_migration_entry_exists(cursor, table_name):
    """Verifica si una entrada para la tabla ya existe en la tabla de migración."""
    select_sql = f"SELECT tablamigrada, ultidreporte FROM `{MIGRATION_TABLE_NAME}` WHERE tabla = %s "
    logger.debug(f"Verificando existencia de entrada para {table_name} en {MIGRATION_TABLE_NAME}...")
    cursor.execute(select_sql, (table_name,))
    result = cursor.fetchone()
    if result:
        return result[0], result[1]
    return None, None

def _update_migration_entry(cursor, table_name, last_copied_id, current_migrada_count):
    """Realiza la operación de UPDATE en la tabla de migración."""
    update_sql = f"""
    UPDATE `{MIGRATION_TABLE_NAME}`
    SET
        `ultidreporte` = %s,
        `fechaupdate` = CURRENT_TIMESTAMP,
        `tablamigrada` = COALESCE(`tablamigrada`, 0) + 1
    WHERE
        `tabla` = %s
    """
    params = (last_copied_id, table_name)
    logger.debug(f"Ejecutando SQL (UPDATE): {update_sql} con params: {params}")
    cursor.execute(update_sql, params)
    return cursor.rowcount

def _insert_migration_entry(cursor, table_name, last_copied_id):
    """Realiza la operación de INSERT en la tabla de migración."""
    insert_sql = f"""
    INSERT INTO `{MIGRATION_TABLE_NAME}` (`tabla`,`fecha`, `ultidreporte`, `tablamigrada`)
    VALUES (%s,NOW(), %s, 1)
    """
    params = (table_name, last_copied_id)
    logger.debug(f"Ejecutando SQL (INSERT): {insert_sql} con params: {params}")
    cursor.execute(insert_sql, params)
    return cursor.rowcount

def update_migration_progress(src_conn, table_name, last_copied_id):
    """
    Inserta o actualiza el progreso de la copia de una tabla en la tabla 'migracion'.
    El contador 'tablamigrada' se incrementa en cada llamada.
    """
    cursor = src_conn.cursor()
    try:
        current_migrada_count, last_id_tracker = _check_migration_entry_exists(cursor, table_name)
        rows_affected = 0

        if current_migrada_count is not None:
            rows_affected = _update_migration_entry(cursor, table_name, last_copied_id, current_migrada_count)
        else:
            rows_affected = _insert_migration_entry(cursor, table_name, last_copied_id)

        src_conn.commit()
        logger.info(f"Progreso guardado y contador 'tablamigrada' actualizado para {table_name}: IdReporte {last_copied_id}. Filas afectadas: {rows_affected}")

    except mysql.connector.Error as err:
        logger.error(f"Error al actualizar el progreso de la migración para {table_name}: {err}")
    finally:
        cursor.close()
        
def get_ordered_log_tables_for_migration(conn):
    """
    Obtiene la lista de tablas LOG_ para la migración, basada en la última posición.
    """
    cursor = conn.cursor()
    
    sql_query = f"""
        SELECT
            CONCAT('LOG_', t.Reporte_Iddisp) AS table_name
        FROM
            `ultima_posicion` AS t
        ORDER BY
            t.Reporte_fechagps DESC
        LIMIT 500
    """
    
    logger.info(f"Ejecutando SQL para obtener la lista de tablas a migrar...")
    
    try:
        cursor.execute(sql_query)
        # La tupla ahora solo tiene 1 elemento (el nombre de la tabla)
        tables_info = [row[0] for row in cursor.fetchall()]
        
        log_tables_found = tables_info
        logger.info(f"Tablas 'LOG_' encontradas en la tabla `ultima_posicion`: {', '.join(log_tables_found) if log_tables_found else 'Ninguna'}")
        
        return tables_info
    except mysql.connector.Error as err:
        logger.error(f"Error al obtener la lista de tablas de la tabla `ultima_posicion`: {err}")
        return []
    finally:
        cursor.close()

def get_max_idreporte_from_dest(dest_conn, table):
    """
    Obtiene el valor máximo de 'IdReporte' de una tabla en la base de datos de destino.
    Retorna 0 si la tabla no existe o está vacía, o el valor máximo encontrado.
    """
    if not table_exists(dest_conn, table):
        return 0
    cursor = dest_conn.cursor()
    try:
        sql_query = f"SELECT MAX(IdReporte) FROM `{table}`"
        cursor.execute(sql_query)
        max_id = cursor.fetchone()[0]
        return max_id if max_id is not None else 0
    except mysql.connector.Error as err:
        logger.warning(f"No se pudo obtener MAX(IdReporte) para la tabla {table} en destino: {err}. Asumiendo 0.")
        return 0
    finally:
        cursor.close()

def copy_data(src_conn, dest_conn, table):
    """
    Copia los datos de la tabla de origen a la de destino del último mes.
    Retorna el último IdReporte copiado.
    """
    start_time = time.time()
    copied = 0
    last_id_in_current_batch = None
    
    # Consulta para obtener datos del último mes del origen
    select_sql = f"SELECT * FROM `{table}` WHERE Reporte_FechaGPS >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH) ORDER BY IdReporte ASC"
    params = ()
    logger.info(f"Copiando los datos del último mes de '{table}'.")

    src_cursor = None
    try:
        src_cursor = src_conn.cursor(buffered=False)
        logger.debug(f"Ejecutando SQL: {select_sql}")
        src_cursor.execute(select_sql, params)
        
        # Obtener los nombres de las columnas para el INSERT
        cols = [desc[0] for desc in src_cursor.description]
        try:
            id_reporte_col_index = cols.index('IdReporte')
        except ValueError:
            logger.error(f"La columna 'IdReporte' no se encontró en la tabla {table}.")
            return None

        placeholders = ",".join(["%s"] * len(cols))
        insert_sql = f"INSERT IGNORE INTO `{table}` ({', '.join(cols)}) VALUES ({placeholders})"
        logger.debug(f"Preparando INSERT SQL: {insert_sql}")

        batch = []
        
        # Estimar el número de filas totales para la copia
        count_cursor = src_conn.cursor()
        count_sql = f"SELECT COUNT(*) FROM `{table}` WHERE Reporte_FechaGPS >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)"
        count_cursor.execute(count_sql)
        total_rows_to_copy = count_cursor.fetchone()[0]
        count_cursor.close()

        if total_rows_to_copy == 0:
            logger.info(f"No hay datos en el último mes para copiar en la tabla {table}.")
            return get_max_idreporte_from_dest(dest_conn, table)

        logger.info(f"Copiando datos de {table}: {total_rows_to_copy} filas en total.")
        
        for row in src_cursor:
            batch.append(row)
            last_id_in_current_batch = row[id_reporte_col_index]
            
            if len(batch) >= BATCH_SIZE:
                with dest_conn.cursor() as dest_cur:
                    dest_cur.executemany(insert_sql, batch)
                dest_conn.commit()
              
                copied += len(batch)
                batch.clear()
                
                elapsed = time.time() - start_time
                rate = copied / elapsed if elapsed > 0 else 0
                eta = (total_rows_to_copy - copied) / rate if rate > 0 else None
                logger.info(f"  Copiadas {copied}/{total_rows_to_copy}. Último ID: {last_id_in_current_batch}. ETA: {eta:.1f}s" if eta else f"  Copiadas {copied}/{total_rows_to_copy}. Último ID: {last_id_in_current_batch}.")

        if batch:
            with dest_conn.cursor() as dest_cur:
                dest_cur.executemany(insert_sql, batch)
            dest_conn.commit()
            copied += len(batch)
            
    except Exception as e:
        logger.exception(f"Error inesperado durante la copia de datos para {table}: {e}")
        return None
    finally:
        if src_cursor:
            src_cursor.close()
    
    logger.info(f"Tabla {table} completada en {time.time() - start_time:.1f}s. Último ID copiado: {last_id_in_current_batch}.")
    return last_id_in_current_batch if last_id_in_current_batch is not None else get_max_idreporte_from_dest(dest_conn, table)

def main():
    overall_start = time.time()
    src = None
    dst = None
    try:
        logger.info("Conectando a origen...")
        src = mysql.connector.connect(**SOURCE_CONFIG)
        logger.info("Conectado al origen.")

        logger.info("Conectando a destino...")
        dst = mysql.connector.connect(**DEST_CONFIG)
        logger.info("Conectado al destino.")

        create_migration_table_if_not_exists(src)

        tables_info = get_ordered_log_tables_for_migration(src)
        logger.info(f"Tablas 'LOG_' encontradas en origen para procesar: {len(tables_info)}")

        for table_name in tables_info:
            logger.info(f"\n--- Iniciando revisión y procesamiento para tabla: {table_name} ---")
            
            last_id_for_tracker = None

            try:
                # CREACIÓN DE LA TABLA EN DESTINO ANTES DE CUALQUIER COPIA
                create_table_if_not_exists_on_dest(dst, table_name)
                
                copied_id = copy_data(src, dst, table_name)
                
                if copied_id is not None:
                    last_id_for_tracker = copied_id
                
            except Exception as e:
                logger.exception(f"Error inesperado al procesar tabla {table_name}: {e}. Intentando reconectar a la base de datos de origen.")
                if src and src.is_connected(): src.close()
                src = mysql.connector.connect(**SOURCE_CONFIG)
                logger.info("Reconexión a origen exitosa. Continuando con la siguiente tabla.")
            finally:
                if last_id_for_tracker is not None:
                    update_migration_progress(src, table_name, last_id_for_tracker)
                logger.info(f"--- Revisión y contador para tabla {table_name} finalizado en este ciclo. ---")
            
        total_time = time.time() - overall_start
        logger.info(f"Proceso de migración de tablas LOG_ completado en {total_time:.1f}s.")

    except mysql.connector.Error as err:
        logger.error(f"Error fatal de MySQL en el proceso principal: {err}")
        sys.exit(1)
    except Exception as e:
        logger.error(f"Error inesperado fatal en el proceso principal: {e}")
        sys.exit(1)
    finally:
        if 'src' in locals() and src and src.is_connected(): src.close()
        if 'dst' in locals() and dst and dst.is_connected(): dst.close()
        logger.info("Conexiones a base de datos cerradas.")


if __name__ == '__main__':
    main()