#!/usr/bin/perl

# ---------------------------------------------------------------
# Prontus CMS
# http://www.prontus.cl
# by Altavoz.net
#
# licensed under LGPL license.
# http://www.prontus.cl/license.html
# ---------------------------------------------------------------

# -------------------------------COMENTARIO GLOBAL---------------
# ---------------------------------------------------------------
# PROPOSITO .
# -----------
# Este script corre por consola, y tiene tres modos de funcionamiento:
# - Indexación masiva: si se le pasa el parámetro "full", indexa todos los documentos que puede.
#   Busca en la base de datos los artículos con alta, y los procesa
#   en batches del siguiente modo
#   - genera una salida NDJSON por batch
#   - gatilla un proceso curl por batch
# - Indexación de un artículo: si se le pasa como parámetro un ts, indexa ese documento (crea o actualiza el índice)
# - Eliminación de un artículo del índice: si se le pasa como parámetros un ts y la string DELETE, borra el documento del índice de búsqueda.
#
# Configuración:
#   Este script indexa contenidos basándose en
# - el archivo de configuración [webroot]/[prontus]/cpan/[prontus]-elasticsearch.cfg
#   Ese archivo debe existir, y su contenido debería ser al menos este:
# -----------
#   ELASTICSEARCH_FIDS = "fid_foto.html fid_noticia.html fid_video.html" # fids a indexar separados por espacios
#   ELASTICSEARCH_HOST = "10.0.0.20" # ip o hostname del servidor donde está elasticsearch
#   ELASTICSEARCH_HOST_PROTOCOL = "http" # protocolo del server elasticsearch.
#   ELASTICSEARCH_PORT = "9200" # puerto HTTP del servidor elasticsearch
#   ELASTICSEARCH_INDEX = "prontus_next" # nombre del index a crear. Aquí quedarán indexados los documentos de este prontus.
#   ELASTICSEARCH_TEXTVARS = "_txt_bajada:3 vtxt_cuerpo:2..." [nombre del campo]:[factor de boost].
#   ELASTICSEARCH_TITLEVAR = "_txt_titular" [por defecto el factor de boost es 10]
#   ELASTICSEARCH_METADATA[1..20] = ... # otros campos específicos. Acá habría que distinguir el tipo de campo.
# ----------
# - la configuración más abajo. Para adaptarlo a cada sitio en particular, modificar aquí al menos la configuración
#   $NDJSON_TEMPLATE y luego la sub procesar_sql agregando el código necesario para extraer cada campo del xml del artículo.
# Para usar este sistema, copiar este script a uno sin el nombre "-template" y ejecutarlo por consola:
#   Indexación masiva:
#       perl [path]/cgi-cpn/prontus_indexar_elasticsearch.pl [prontus] full # borra el indice antes de reindexar
#   Indexación específica:
#       perl [path]/cgi-cpn/prontus_indexar_elasticsearch.pl [prontus] [ts]
#   Borrado de índice de un artículo:
#       perl [path]/cgi-cpn/prontus_indexar_elasticsearch.pl [prontus] [ts] DELETE
# ---------------------------------------------------------------
use utf8;
use strict;

BEGIN {
    use FindBin qw($Bin);
    my $pathLibsProntus = $Bin;
    unshift(@INC,$pathLibsProntus);
    $pathLibsProntus =~ s/\/[^\/]+$//;
    unshift(@INC,$pathLibsProntus);
}
# Captura STDERR
use lib_prontus;
use lib_stdlog;
&lib_stdlog::set_stdlog($0, 51200);

use prontus_varglb; &prontus_varglb::init();
use glib_html_03;
use glib_fildir_03;
use glib_dbi_03;
use glib_str_03;
use glib_hrfec_03;
use lib_logproc;
use Artic;
use Encode;
use POSIX; # para floor.
use JSON;
use LWP::ConnCache;
use LWP::UserAgent;
use HTTP::Request;
#use Time::HiRes;
#use Data::Dumper; # para debugs

# variables globales de config por defecto.
# Si no se encuentran alternativas en el archivo de config [prontus]-elasticsearch.cfg, se usarán estas.
my $INVOCACION; # web o cli
my $ELASTICSEARCH_HOST_PROTOCOL = 'http';
my $ELASTICSEARCH_HOST = "localhost";
my $ELASTICSEARCH_PORT = "9200";
my $ELASTICSEARCH_INDEX = 'index_prontus';
my $ELASTICSEARCH_BATCH_CHUNK = 50; # procesamos 50 articulos por vez. En pruebas, CURL se cae si se le pasan strings demasiado largas. Reducir este valor o cambiar la cantidad/peso de los campos a indexar si sucede eso.
my %CAMPOS_PREDEFINIDOS; # Se rellenan con los campos predefinidos para comparar contra ELASTICSEARCH_SEARCH_META, para evitar errores al crear los mappings.

# Variables globales para configurar la indexación. Modificar acá.

# Configuraciones globales del index, como mappings, etc.
# Deberíamos crear el índice, luego crear los mapeos con JSON_TEMPLATE_BASE, luego indexar.
# Este doble indexado se hace para mostrar correctamente resultados con y sin acentos en la misma palabra. https://stackoverflow.com/questions/38452791/elasticsearch-insensitive-search-accents
# TODO: revisar que se dé más importancia a la palabra tal cual está escrita.
my $SETTINGS = '{
    "settings": {
        "analysis": {
            "analyzer": {
                "folding": {
                    "tokenizer": "standard",
                    "filter": [
                        "lowercase",
                        "asciifolding"
                    ]
                }
            }
        }
    }
}
';
my $JSON_TEMPLATE_BASE = '
{
    "properties": {
        "_txt_titular": {
            "type": "text",
            "analyzer": "spanish",
            "fields": {
                "std":{
                    "type": "text",
                    "analyzer": "folding"
                }
            }
        },
        "_txt_bajada": {
            "type": "text",
            "analyzer": "spanish",
            "fields": {
                "std":{
                    "type": "text",
                    "analyzer": "folding"
                }
            }
        },
        "_fechap": {
            "type": "long"
        },
        "_priorizar": {
            "type": "boolean"
        }
        %%_properties%%
    }
}';

# JSON de base para campos de texto indexable y analizado en un idioma (por ahora español).
# Notar que se guardan dos campos: uno analizado en español, otro con analizador standard para
# matches más amplios.
my $JSON_TEXT_TEMPLATE = '    "%%nombre_campo%%": {
        "type": "text",
        "index": true,
        "analyzer": "spanish",
        "fields": {
            "std":{
                "type": "text",
                "analyzer": "folding"
            }
        }
    }';

# JSON de base para campos de texto no indexado.
my $JSON_KEYWORD_TEMPLATE = '
    "%%nombre_campo%%": {
        "type": "keyword",
        "index": false
    }
';

# JSON de base para campos de fecha.
my $JSON_DATE_TEMPLATE = '
    "%%nombre_campo%%": {
        "type": "date"
    }
';

# JSON de base para campos numéricos.
my $JSON_NUMBER_TEMPLATE = '
    "%%nombre_campo%%": {
        "type": "long"
    }
';

# Template para los campos a indexar. Este es un newline-delimited JSON, donde la primera línea indica el índice en que crear el documento, y la segunda contiene los datos a indexar.
# cada campo tiene el nombre de una marca Prontus.
# El contenido aquí son los campos de base, y pueden sumarse a éstos los campos en ELASTICSEARCH_TEXTVARS y ELASTICSEARCH_METADATA, por ejemplo.
my $NDJSON_TEMPLATE = '{"index": {"_index":"%%_index%%", "_id": "%%_ts%%"}}
{"_txt_titular": "%%_titular%%", "_txt_bajada": "%%_bajada%%", "_fileurl": "%%url%%", "_seccion1": "%%_seccion1%%", "_nom_seccion1": "%%_nom_seccion1%%", "_tema1": "%%_tema1%%", "_nom_tema1": "%%_nom_tema1%%", "_subtema1": "%%_subtema1%%", "_nom_subtema1": "%%_nom_subtema1%%", "_tags":[%%_tags%%], "_tagnames": [%%_tagnames%%], "_metadata_img": "%%_metadata_img%%", "_fechap":"%%_fechap%%", "_priorizar":"%%_priorizar%%" %%_otros_campos%%}
'; # NOTA: *no* va coma antes de %%_otros_campos%%, ya que esa variable puede estar vacía.

my $DIRPRONTUS;
my $BD;

main: {
    &lib_prontus::setUtf8();

    $prontus_varglb::PRONTUS_ID = $ARGV[0];
    $INVOCACION = $ARGV[1] || 'cli';

    my $ts = $ARGV[2] || '';
    my $action = $ARGV[3] || '';
    #print "invocacion [$INVOCACION], ts: [$ts], action [$action]\n";

    if ($prontus_varglb::PRONTUS_ID eq '') {
        print "[error] Debe indicar el nombre del Prontus. Uso: <script> <prontus> <cli|web> <full|anio|aniomes|ts> [DELETE]\n La opción DELETE aplica solamente para des indexar un artículo específico. El índice se recrea cuando se hace una reindexación \"full\".";
        exit;
    }

    # Carga variables de configuracion.
    my $msg_load = &lib_prontus::load_config_by_id($prontus_varglb::PRONTUS_ID);
    if ($msg_load ne '') {
        &gestionar_errores($msg_load, 1);
        exit;
    }

    $DIRPRONTUS = "$prontus_varglb::DIR_SERVER$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'}$prontus_varglb::DIR_ARTIC";
    if (! -d $DIRPRONTUS) {
        &gestionar_errores("[error] Directorio de artículos no valido: [$DIRPRONTUS]", 1);
        exit;
    }

    # Establece log file
    $lib_logproc::LOG_FILE = "$prontus_varglb::DIR_SERVER$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_CPAN'}/procs/prontus_elasticsearch_reindex_log.html";

    # Conectar a BD
    my $msg_err_bd;
    ($BD, $msg_err_bd) = &lib_prontus::conectar_prontus_bd();
    if (! ref($BD)) {
        &gestionar_errores("[error]: $msg_err_bd", 1);
        exit;
    }

    $ELASTICSEARCH_HOST = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_HOST'};
    $ELASTICSEARCH_HOST_PROTOCOL = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_HOST_PROTOCOL'};
    $ELASTICSEARCH_PORT = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_PORT'};
    $ELASTICSEARCH_INDEX = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_INDEX'};

    # fids prontus a indexar.
    my @fids_validos = split(/[, ]/, $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_FIDS'});
    my @tmp;
    # quitamos las eventuales extensiones de FID.
    foreach my $fid (@fids_validos) {
        if ($fid =~ /^(\w+)\.\w+$/) {
            push (@tmp, $1);
        } else {
            push (@tmp, $fid);
        }
    }
    @fids_validos = @tmp;

    # obtenemos:
    # - la string json que define cada campo en el mapping del index
    # - la string json que usaremos al indexar cada documento, con sus campos.
    # - un array de campos para procesar al leer los datos de cada documento.
    my ($index_properties, $string_campos, @campos_procesar) = &get_campos_procesar();
    #print STDERR "Properties: [$index_properties]\n\n";

    $JSON_TEMPLATE_BASE =~ s/%%_properties%%/$index_properties/g;
    $NDJSON_TEMPLATE =~ s/%%_otros_campos%%/$string_campos/g;

    my $ua = LWP::UserAgent->new;
    $ua->agent('Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3'); # default es "libwww-perl/#.##"
    my $conn_cache = LWP::ConnCache->new;
    $ua->conn_cache($conn_cache);
    my ($req, $base_url, $url, $header, $method);
    $header = ['Content-Type' => "application/json; charset=UTF-8"];
    $base_url = $url = "$ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX";
    # indexación masiva
    if ($ts eq 'full') { # procesamos todos los documentos válidos.
        my $command;
        my $start = 0;
        my $response; # para los resultados de los comandos.
        my $msg;
        if ($action =~ /^\d+$/) { # comenzar a procesar desde este item.
            $start = $action;
        } else { #recreamos el índice de cero.
            # borramos y recreamos antes el índice existente.
            $method = "DELETE";
            $req = HTTP::Request->new($method, $url, $header);
            $response = $ua->request($req);
            if (! $response->is_success && $response->code ne "404") {
                &gestionar_errores("Error borrando el índice $ELASTICSEARCH_INDEX en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT: [$method] [$url] [".$response->status_line."]", 1);
                exit;
            }
            $response = decode_json($response->content);
            if ($response->{'error'} && $response->{'error'}{'type'} ne 'index_not_found_exception') {
                &gestionar_errores("Error borrando el índice 2 $ELASTICSEARCH_INDEX en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT: [$response->{'error'}{'type'}] [$response->{'error'}{'reason'}]", 1);
                exit;
            }
            &gestionar_mensajes("Indice anterior eliminado en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX", 0);
            $response = '';

            # creamos el índice
            $method = "PUT";
            $req = HTTP::Request->new($method, $url, $header, $SETTINGS);
            #$command = "curl -H 'Content-Type: application/json' -XPUT $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX -d'$SETTINGS'";
            $response = $ua->request($req);
            if (!$response->is_success) {
                my $extra = '';
                if ($response->{'_content'}{'error'}{'root_cause'}) {
                    $extra = ", ".$response->{'_content'}{'error'}{'root_cause'}{'reason'};
                }
                &gestionar_errores("Error configurando el índice $ELASTICSEARCH_INDEX en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT: [".$response->status_line."$extra]", 1);
                exit;
            }
            $response = decode_json($response->content);
            if ($response->{'error'}) {
                &gestionar_errores("Error creando el índice $ELASTICSEARCH_INDEX en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT: [$response->{'error'}{'type'}] [$response->{'error'}{'reason'}]", 1);
                exit;
            }
            &gestionar_mensajes("Indice generado en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX", 0);
            $response = '';

            # configuramos el índice con properties y mappings
            if ($JSON_TEMPLATE_BASE ne '') {
                # creamos el índice
                $method = "POST";
                $url = "$base_url/_mapping";
                $req = HTTP::Request->new($method, $url, $header, $JSON_TEMPLATE_BASE);
                $response = $ua->request($req);
                if (!$response->is_success) {
                    &gestionar_errores("Error configurando el índice 1 $ELASTICSEARCH_INDEX en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT: [".$response->status_line."]", 1);
                    exit;
                }
                $response = decode_json($response->content);
                if ($response->{'error'}) {
                    &gestionar_errores("Error configurando el índice 2 $ELASTICSEARCH_INDEX en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT: [$response->{'error'}{'type'}] [$response->{'error'}{'reason'}]", 1);
                    exit;
                }
                $response = '';
                &gestionar_mensajes("Mappings creados en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX", 0);
            } else {
                &gestionar_mensajes("No está definido el TEMPLATE_BASE para configurar el índice", 0);
            }
        }

        my $cantidad_articulos_inicial = &contar_articulos(\@fids_validos);
        $cantidad_articulos_inicial -= $start;
        &gestionar_mensajes("Se indexarán $cantidad_articulos_inicial artículos", 0);
        # print STDERR "Cantidad: $cantidad_articulos\n";
        # $cantidad_articulos_inicial = 300; # para pruebas cortas

        # procesamos los artículos en bloques
        my $template = "";
        my $string_articulos_a_indexar = '';
        $ELASTICSEARCH_BATCH_CHUNK = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_BATCH_CHUNK'};
        my $cantidad_articulos = $cantidad_articulos_inicial;
        my ($start_time, $end_time, $diff);
        while ($cantidad_articulos > 0) {
            #$start_time = Time::HiRes::gettimeofday();
            $string_articulos_a_indexar = &generar_json_articulos(\@fids_validos, $start, $ELASTICSEARCH_BATCH_CHUNK, '', \@campos_procesar);
            #$end_time= Time::HiRes::gettimeofday();
            #$diff = sprintf('%.3f', $end_time - $start_time);
            #print "generar_json_articulos tomo $diff segundos\n" if ($INVOCACION eq 'cli');
            $start += $ELASTICSEARCH_BATCH_CHUNK;
            #$start_time = Time::HiRes::gettimeofday();
            &indexar_articulos($string_articulos_a_indexar, $ua);
            #$end_time= Time::HiRes::gettimeofday();
            #$diff = sprintf('%.3f', $end_time - $start_time);
            #print "indexar_articulos tomo $diff segundos\n" if ($INVOCACION eq 'cli');

            #print STDERR "{\"chunk\":$ELASTICSEARCH_BATCH_CHUNK,\"start\": $start, \"remanente\": $cantidad_articulos, \"indexados\":".($cantidad_articulos_inicial - $cantidad_articulos)."}\n";

            $cantidad_articulos -= $ELASTICSEARCH_BATCH_CHUNK;
            if ($cantidad_articulos < 0) {
                $cantidad_articulos = 0;
            }
            &gestionar_mensajes("Artículos indexados: ".($cantidad_articulos_inicial - $cantidad_articulos).". Restantes: $cantidad_articulos.", 0);
        }
        &gestionar_mensajes("Proceso terminado.", 1);
    } elsif ($ts =~ /^\d{4}$/) { # procesamos un año.
        my $anio = $ts;
        my $cantidad_articulos_inicial = &contar_articulos(\@fids_validos, $anio);
        &gestionar_mensajes("Se indexarán $cantidad_articulos_inicial artículos.", 0);

        # print STDERR "Cantidad: $cantidad_articulos\n";
        # $cantidad_articulos = 100; # para pruebas cortas
        # procesamos los artículos en bloques
        my $start = 0;
        my $template = "";
        my $string_articulos_a_indexar = '';
        $ELASTICSEARCH_BATCH_CHUNK = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_BATCH_CHUNK'};
        my $cantidad_articulos = $cantidad_articulos_inicial;
        while ($cantidad_articulos > 0) {
            $string_articulos_a_indexar = &generar_json_articulos(\@fids_validos, $start, $ELASTICSEARCH_BATCH_CHUNK, $anio, \@campos_procesar);
            $start += $ELASTICSEARCH_BATCH_CHUNK;
            &indexar_articulos($string_articulos_a_indexar, $ua);
            &gestionar_mensajes("Artículos indexados: ".($cantidad_articulos_inicial - $cantidad_articulos).". Restantes: $cantidad_articulos.", 0);

            $cantidad_articulos -= $ELASTICSEARCH_BATCH_CHUNK;
        }
        &gestionar_mensajes("Proceso terminado.", 1);
    } elsif ($ts =~ /^\d{14}$/) { # procesamos un documento específico
        if ($action eq 'DELETE') { # eliminamos el documento
            my $command = "curl -XDELETE $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX/_doc/$ts";
            my $response = `$command`;
            my $msg;
            if ($?) {
                $msg = "Error eliminando el artículo [$ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX/_doc/$ts]: $?";
                &gestionar_errores($msg, 1);
                exit;
            }
            $response = decode_json($response);
            if ($response->{'error'}) {
                $msg = "Error eliminando el artículo [$ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX/_doc/$ts]: [$response->{'error'}{'type'}] [$response->{'error'}{'reason'}]";
                &gestionar_errores($msg, 1);
                exit;
            }
            $response = '';
            #print "Eliminando artículo [$ts] del índice.\n";
        } else { # creamos o actualizamos el índice del artículo.
            my $articulo_a_indexar = &generar_json_articulo(\@fids_validos, $ts, \@campos_procesar);
            if ($articulo_a_indexar) {
                &gestionar_mensajes("Creando o actualizando artículo [$ts] en el índice.", 0);
                &indexar_articulos($articulo_a_indexar, $ua);
            }
        }
    } else {
        &gestionar_errores("[error]: Invocación inválida. Uso: <script> <prontus> <cli|web> <full|anio|aniomes|ts> [DELETE]", 1);
        exit;
    }
}

# devuelve el conteo de artículos de los tipos especificados con alta = 1 en bd.
sub contar_articulos {
    my @fids_validos = @{$_[0]}; # referencia a array.
    my $anio = $_[1];
    my @params;
    my $ts_now = &glib_hrfec_03::get_dtime_pack4();
    push (@params, $ts_now);
    push (@params, $ts_now); # se evalúa dos veces

    my $filtros;
    if (scalar @fids_validos > 0) {
        push (@params, @fids_validos);
        my $param_str = ', ?' x scalar @fids_validos;
        $param_str = substr $param_str, 2;
        $filtros = " AND ART_TIPOFICHA IN ($param_str) ";
    } else {
        $filtros = "";
    }

    # si pasaron un año, indexamos solamente ese año.
    my ($fecha_inicio, $fecha_fin);
    if ($anio =~ /^\d{4}$/) {
        $fecha_inicio = "${anio}0101";
        $fecha_fin = "${anio}1231";
        push (@params, $fecha_inicio);
        push (@params, $fecha_fin);
        $filtros .= " AND (ART_FECHAP >= ? AND ART_FECHAP <= ? )";
    }

    my $sql = "SELECT COUNT(*) FROM ART WHERE ART_ALTA = 1 AND ART_INDEXAR = 1 AND (CONCAT(ART_FECHAP,ART_HORAP) <= ? AND CONCAT(ART_FECHAE, ART_HORAE) >= ?) $filtros";

    my $conteo;
    my $salida = &glib_dbi_03::ejecutar_sql_bind_param($BD, $sql, \@params, \($conteo,));
    $salida->fetch;
    $salida->finish;
    return $conteo;
}

# Recibe un array de FIDs que se pueden indexar, y un TS de un artículo.
# devuelve un string JSON con los datos ingresados según template.
# devuelve '' si el artículo no corresponde a un fid indexable.
sub generar_json_articulo {
    my @fids_validos = @{$_[0]}; # referencia a array.
    my $ts = $_[1];
    my @campos_procesar = @{$_[2]};

    my @params;

    my $ts_now = &glib_hrfec_03::get_dtime_pack4();
    push (@params, $ts_now);
    push (@params, $ts_now); # se evalúa dos veces

    if ($ts !~ /\d{14}/) {
        return '';
    } else {
        push (@params, $ts);
    }

    my $filtros;
    if (scalar @fids_validos > 0) {
        push (@params, @fids_validos);
        my $param_str = ', ?' x scalar @fids_validos;
        $param_str = substr $param_str, 2; # eliminamos la primera ', '
        $filtros = " AND ART_TIPOFICHA IN ($param_str) ";
    } else {
        $filtros = "";
    }

    my $sql = "SELECT SUBSTR(ART_ID, 1, 8) AS ART_FECHAC, ART_ID, ART_TIPOFICHA, ART_TITU, ART_BAJA, ART_PRIORIZAR FROM ART WHERE ART_ALTA = 1 AND ART_INDEXAR = 1 AND (CONCAT(ART_FECHAP,ART_HORAP) <= ? AND CONCAT(ART_FECHAE, ART_HORAE) >= ?) AND ART_ID = ? $filtros ORDER BY ART_ID LIMIT 1";
    #print STDERR "$sql\n";
    my $json_articulo = &procesar_sql($sql, \@params, $ts, \@campos_procesar);

    return $json_articulo;
}

# Recibe una lista de fids válidos para indexar, y los valores de inicio y límite de procesamiento para la query sql
# que buscará los artículos.
# devuelve un string JSON con los datos ingresados según template.
sub generar_json_articulos {
    my @fids_validos = @{$_[0]}; # referencia a array.
    my $start = $_[1];
    my $limit = $_[2];
    my $anio = $_[3];
    my @campos_procesar = @{$_[4]};

    my @params;

    my $ts_now = &glib_hrfec_03::get_dtime_pack4();
    push (@params, $ts_now);
    push (@params, $ts_now); # se evalúa dos veces

    my $filtros;
    if (scalar @fids_validos > 0) {
        push (@params, @fids_validos);
        my $param_str = ', ?' x scalar @fids_validos;
        $param_str = substr $param_str, 2; # eliminamos la primera ', '
        $filtros = " AND ART_TIPOFICHA IN ($param_str) ";
    } else {
        $filtros = "";
    }

    # si pasaron un año, indexamos solamente ese año.
    my ($fecha_inicio, $fecha_fin);
    if ($anio =~ /^\d{4}$/) {
        $fecha_inicio = "${anio}0101";
        $fecha_fin = "${anio}1231";
        push (@params, $fecha_inicio);
        push (@params, $fecha_fin);
        $filtros .= " AND (ART_FECHAP >= ? AND ART_FECHAP <= ? )";
    }

    push (@params, $start);
    push (@params, $limit);
    my $sql = "SELECT SUBSTR(ART_ID, 1, 8) AS ART_FECHAC, ART_ID, ART_TIPOFICHA, ART_TITU, ART_BAJA, ART_PRIORIZAR FROM ART WHERE ART_ALTA = 1 AND ART_INDEXAR = 1 AND (CONCAT(ART_FECHAP,ART_HORAP) <= ? AND CONCAT(ART_FECHAE, ART_HORAE) >= ?) $filtros ORDER BY ART_ID LIMIT ?, ?";
    my $json_articulos = &procesar_sql($sql, \@params, '', \@campos_procesar);

    return $json_articulos;
}

# Recibe una sentencia SQL con parámetros y procesa la query, armando una string json con los resultados.
# Si recibe un ts prontus, busca los datos de ese artículo solamente
# De otro modo procesa todos los artículos con alta.
# devuelve una string NDJSON.
sub procesar_sql {
    my $sql = $_[0];
    my $params = $_[1];
    my $ts = $_[2];
    my @campos_procesar = @{$_[3]};

    my ($fechac, $fid, $titular, $bajada, $vtxt_cuerpo, $es_priorizado);
    my $salida = &glib_dbi_03::ejecutar_sql_bind_param($BD, $sql, $params, \($fechac, $ts, $fid, $titular, $bajada, $es_priorizado));
    my $json_articulos = '';
    my $ndjson_template = $NDJSON_TEMPLATE;
    my $buffer = '';
    my $path = '';
    my $xmlbuffer = '';
    my %hashprontus;
    my $friendly_url = '';
    while ($salida->fetch) {
        $titular = &procesar_campo($titular);
        $bajada = &procesar_campo($bajada);

        $buffer = $ndjson_template;
        $buffer =~ s/%%_index%%/$ELASTICSEARCH_INDEX/;
        $buffer =~ s/%%_ts%%/$ts/;
        $buffer =~ s/%%_titular%%/$titular/;
        $buffer =~ s/%%_bajada%%/$bajada/;

        if ($es_priorizado == 0) {
            $es_priorizado = 'false';
        } else {
            $es_priorizado = 'true';
        }
        $buffer =~ s/%%_priorizar%%/$es_priorizado/;

        $path = "$prontus_varglb::DIR_SERVER/$prontus_varglb::PRONTUS_ID/site/artic/$fechac/xml/$ts.xml";
        my $xmlbuffer = glib_fildir_03::read_file($path, 1);
        $xmlbuffer =~ s/.+?<_private>(.*?)<\/_private>/$1/is;
        $xmlbuffer =~ s/<_public>(.*?)<\/_public>.+/$1/is;
        my %hashprontus = lib_prontus::getCamposXml($xmlbuffer);
        $friendly_url = &get_friendly_url($ts, \%hashprontus);

        # campos predefinidos a indexar o agregar: secciones, tags, imagen para resultados, fechap.
        my $seccion1 = $hashprontus{'_seccion1'} || '';
        my $nom_seccion1 = $hashprontus{'_nom_seccion1'};
        my $tema1 = $hashprontus{'_tema1'} || '';
        my $nom_tema1 = $hashprontus{'_nom_tema1'};
        my $subtema1 = $hashprontus{'_subtema1'} || '';
        my $nom_subtema1 = $hashprontus{'_nom_subtema1'};
        my $metadata_img = $hashprontus{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_METADATA_IMG"}};
        my $fechap = $hashprontus{'_fechap'};

        # separamos los tags y tagnames y los volvemos a unir rodeados por comillas.
        my @tags = split(',', $hashprontus{'_tags'});
        my $tags = '"'.join('","', @tags).'"';
        my @tagnames = split(',', $hashprontus{'_tagnames'});
        my $tagnames = '"'.join('","', @tagnames).'"';

        $buffer =~ s/%%_seccion1%%/$seccion1/;
        $buffer =~ s/%%_nom_seccion1%%/$nom_seccion1/;
        $buffer =~ s/%%_tema1%%/$tema1/;
        $buffer =~ s/%%_nom_tema1%%/$nom_tema1/;
        $buffer =~ s/%%_subtema1%%/$subtema1/;
        $buffer =~ s/%%_nom_subtema1%%/$nom_subtema1/;
        $buffer =~ s/%%_tags%%/$tags/;
        $buffer =~ s/%%_tagnames%%/$tagnames/;
        $buffer =~ s/%%_metadata_img%%/$metadata_img/;
        $buffer =~ s/%%_fechap%%/$fechap/;
        $buffer =~ s/%%url%%/$friendly_url/;

        %CAMPOS_PREDEFINIDOS = {
            '_seccion1' => 1,
            '_nom_seccion1' => 1,
            '_tema1' => 1,
            '_nom_tema1' => 1,
            '_subtema1' => 1,
            '_nom_subtema1' => 1,
            '_fechap' => 1,
            '_tags' => 1,
            '_tagnames' => 1,
        };

        # procesamos los demás campos agregados:
        foreach my $nombre_campo (@campos_procesar) {
            next if ($CAMPOS_PREDEFINIDOS{$nombre_campo});
            my $campo = &procesar_campo($hashprontus{$nombre_campo});
            $campo = substr($campo, 0, $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_MAXCARS'});
            $buffer =~ s/%%$nombre_campo%%/$campo/g;
        }
        $buffer =~ s/%%.*%%//g;
        $json_articulos .= $buffer;
    }
    $json_articulos .= "\n";
    return $json_articulos;
}

# Recibe una string NDJSON con los artículos a indexar y gatilla una llamada a curl para ejecutar esa acción.
# TODO: implementar API Perl.
sub indexar_articulos {
    my ($json_indexar, $ua) = @_;
    # el parámetro "refresh" actualiza el shard con el documento para que pueda encontrarse inmediatamente.
    my $base_url = "$ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX";
    my $url = $base_url."/_bulk?refresh";
    my $header = ['Content-Type' => "application/json; charset=UTF-8"];
    my $method = "POST";

    my $req = HTTP::Request->new($method, $url, $header, Encode::encode_utf8($json_indexar));

    my $response = $ua->request($req);

    # errores en el comando curl.
    # usualmente cuando falla aquí se debe a que la string de argumentos pasada por línea de comandos a curl es demasiado grande. Hay que jugar entonces con la cantidad de documentos indexados (ELASTICSEARCH_BATCH_CHUNK) o con el máximo de caracteres por campo (ELASTICSEARCH_MAXCARS).
    if (! $response->is_success) {
        &gestionar_errores("Error indexando en $ELASTICSEARCH_HOST_PROTOCOL://$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT/$ELASTICSEARCH_INDEX: [".$response->status_line."]", 1);
        exit;
    }
    # revisamos el resultado para comprobar que indexó.
    $response = decode_json($response->content);
    # errores globales al indexar, e.g. hay un error de sintaxis en el json.
    if ($response->{'error'}) {
        &gestionar_errores("Falló la indexación. $response->{'error'}{'type'}; $response->{'error'}{'reason'}", 1);
        exit;
    }
    # errores particulares al indexar, e.g. incluimos dos veces un campo.
    if ($response->{'errors'}) {
        #print STDERR Dumper($response->{'items'}[0]);
        &gestionar_errores("Falló la indexación del doc _id [$response->{'items'}[0]{'index'}{'_id'}]: Error tipo [$response->{'items'}[0]{'index'}{'error'}{'type'}], Motivo [$response->{'items'}[0]{'index'}{'error'}{'reason'}], Causa: [$response->{'items'}[0]{'index'}{'error'}{'caused_by'}{'reason'}]", 1);
        exit;
    }
}

# Recibe un campo string y lo procesa para agregarlo correctamente
# al ndjson.
sub procesar_campo {
    my $campo = shift;
    $campo =~ s/['"\\]//g;
    $campo = &lib_prontus::get_minitext_value($campo);
    $campo = &lib_prontus::get_jsontext_value($campo);
    &glib_str_03::correct_encode_utf8_string($campo);
    if ($campo eq 'null') {
        $campo = '';
    }
    return $campo;
}

# procesa los campos a indexar (textvars, metadata) desde las variables de config Prontus.
sub get_campos_procesar {
    # hash de campos a indexar y sus factores de boost.
    my %campos_a_indexar = ('_txt_titular' => 6, '_txt_bajada' => 5); # por defecto indexamos el titular con factor boost = 6 y la bajada con boost 5.
    my $index_properties = '';
    my $string_campos = '';
    my @campos_procesar;

    foreach my $campo (split(/\s+/, $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'ELASTICSEARCH_TEXTVARS'})) {
        my $template = $JSON_TEXT_TEMPLATE;
        my ($nombre, $boost) = split(":", $campo);

        next if ($nombre eq '_txt_titular' || $nombre eq '_txt_bajada'); # descartamos el titular y bajada, se indexan por defecto.

        $boost = floor($boost); # convertimos a entero.
        $boost = 1 if (!$boost || $boost < 1); # el factor de potencia debe ser al menos 1.
        $campos_a_indexar{$nombre} = $boost;

        $template =~ s/%%nombre_campo%%/$nombre/g;
        $index_properties .= ",$template";
        $string_campos .= ", \"$nombre\": \"%%$nombre%%\"";
        push (@campos_procesar, $nombre);
    }

    # procesamos campos predefinidos numéricos. Por ahora sin uso. Nota: el timestamp yyyymmdd de fecha de publicación se procesa como campo numérico predefinido.
    my %campos_num = ();
    foreach my $campo (keys %campos_num) {
        my $metadata_template = $JSON_NUMBER_TEMPLATE;
        $campos_a_indexar{$campo} = $campos_num{$campo};
        $metadata_template =~ s/%%nombre_campo%%/$campo/g;
        $index_properties .= ",$metadata_template";
        $string_campos .= ", \"$campo\": \"%%$campo%%\"";
        push (@campos_procesar, $campo);
    }

    # searchable metadata
    for (my $i = 1; $i <= 5; $i++) {
        my $metadata_template = '';
        if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_SEARCH_META$i"} ne '') {
            my $nombre_metadata = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_SEARCH_META$i"};
            my $tipo_metadata = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_SEARCH_META${i}_TIPO"};
            if ($tipo_metadata eq 'date') {
                $metadata_template = $JSON_DATE_TEMPLATE;
            } elsif ($tipo_metadata eq 'float' || $tipo_metadata eq 'number') {
                $metadata_template = $JSON_NUMBER_TEMPLATE;
            } else {
                $metadata_template = $JSON_KEYWORD_TEMPLATE;
            }
            $campos_a_indexar{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_SEARCH_META$i"}} = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_SEARCH_META${i}_TIPO"};
            $metadata_template =~ s/%%nombre_campo%%/$nombre_metadata/g;
            $index_properties .= ",$metadata_template";
            $string_campos .= ", \"$nombre_metadata\": \"%%$nombre_metadata%%\"";
            push (@campos_procesar, $nombre_metadata);
        }
    }

    # otros campos de metadata (no queryable)
    for (my $i = 1; $i <= 10; $i++) {
        my $metadata_template = '';
        if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_METADATA$i"} ne '') {
            my $nombre_metadata = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_METADATA$i"};
            my $tipo_metadata = 'keyword';
            $metadata_template = $JSON_KEYWORD_TEMPLATE;
            $campos_a_indexar{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{"ELASTICSEARCH_METADATA$i"}} = $tipo_metadata;
            $metadata_template =~ s/%%nombre_campo%%/$nombre_metadata/g;
            $index_properties .= ",$metadata_template";
            $string_campos .= ", \"$nombre_metadata\": \"%%$nombre_metadata%%\"";
            push (@campos_procesar, $nombre_metadata);
        }
    }

    return ($index_properties, $string_campos, @campos_procesar);
}

sub print_JSON {
    my $msg = $_[0];
    my $status = defined $_[1] ? $_[1] : 1; # 0 para continuar, 1 para terminar. Default 1.
    my $inprogress = '';
    if ($status == 0) {
        $inprogress = ", \"inprogress\":\"$msg\"";
    }
    my $result_file = "/$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_CPAN'}/procs/elasticsearch_reindex.json";
    $msg = '{"status":'.$status.', "msg":"'.$msg.'"'.$inprogress.'}';
    &glib_fildir_03::write_file("$prontus_varglb::DIR_SERVER$result_file", $msg);
}

sub gestionar_errores {
    my $mensaje = shift;
    my $complete = shift;
    if ($INVOCACION eq 'web') {
        &lib_logproc::add_to_log("[error]: $mensaje");
        &print_JSON($mensaje, $complete);
    } else {
        print STDERR "$mensaje\n";
    }
}

sub gestionar_mensajes {
    my $mensaje = shift;
    my $complete = shift;
    if ($INVOCACION eq 'web') {
        &lib_logproc::add_to_log($mensaje);
        &print_JSON($mensaje, $complete);
    } else {
        print "$mensaje\n";
    }
}

# ---------------------------------------------------------------
# Devuelve la friendly URL correcta para el artículo en cuestión según la configuración de Prontus.
sub get_friendly_url {
    my ($ts, $hashprontus) = @_;

    my %artic_data = ();
    $artic_data{'fid'} = $hashprontus->{'_fid'};
    $artic_data{'custom_slug'} = $hashprontus->{'_custom_slug'};
    $artic_data{'slug'} = $hashprontus->{'_slug'};
    $artic_data{'_url'} = $hashprontus->{'_url'};
    $artic_data{'nom_seccion'} = $hashprontus->{'_nom_seccion1'};
    $artic_data{'nom_tema'} = $hashprontus->{'_nom_tema1'};
    $artic_data{'nom_subtema'} = $hashprontus->{'_nom_subtema1'};

    my $friendly_url = lib_prontus::parse_filef("%%_fileurl%%", $hashprontus->{'_txt_titular'}, $ts, $prontus_varglb::PRONTUS_ID, $hashprontus->{'_plt'}, \%artic_data);

    return $friendly_url;
}
