#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Cleaner de CSVs a partir de reglas de limpieza.

La clase DataCleaner permite limpiar archivos CSVs con datos a partir de la
aplicación de reglas de limpieza.
"""

import pandas as pd
import geopandas as gpd
import pycrs
from dateutil import tz
import arrow
import parsley
from unidecode import unidecode
import unicodecsv
import cchardet
import warnings
import inspect
import re
import os
import subprocess
from functools import partial

from .fingerprint_keyer import group_fingerprint_strings
from .fingerprint_keyer import get_best_replacements, replace_by_key
from .capitalizer import capitalize

from .georef_api import *


class DuplicatedField(ValueError):
    """Salta cuando hay un campo duplicado en el dataset."""

    def __init__(self, value):
        """Crea mensaje de error."""
        msg = "El campo '{}' está duplicado. Campos duplicados no permitidos."
        super(DuplicatedField, self).__init__(msg)


class DataCleaner(object):
    """Crea un objeto DataCleaner cargando un CSV en un DataFrame y expone
    reglas de limpieza para operar sobre las columnas del objeto y retornar un
    CSV limplio."""

    OUTPUT_ENCODING = str("utf-8")
    OUTPUT_SEPARATOR = str(",")
    OUTPUT_QUOTECHAR = str('"')
    INPUT_DEFAULT_ENCODING = str("utf-8")
    INPUT_DEFAULT_SEPARATOR = str(",")
    INPUT_DEFAULT_QUOTECHAR = str('"')
    DEFAULT_SUFIX = "normalizado"

    def __init__(self, input_path, ignore_dups=False, **kwargs):
        """Carga datos a limpiar en un DataFrame, normalizando sus columnas.

        Args:
            input_path (str): Ruta al archivo que se va a limpiar.
            ignore_dups (bool): Ignora los duplicados en colunas
            kwargs: Todos los argumentos que puede tomar `pandas.read_csv`
        """
        default_args = {
            'encoding': self._get_file_encoding(input_path),
            'sep': self.INPUT_DEFAULT_SEPARATOR,
            'quotechar': self.INPUT_DEFAULT_QUOTECHAR
        }
        default_args.update(kwargs)

        # chequea que no haya fields con nombre duplicado
        if not ignore_dups and input_path.endswith('.csv'):
            self._assert_no_duplicates(input_path,
                                       encoding=default_args['encoding'],
                                       sep=default_args['sep'],
                                       quotechar=default_args['quotechar'])

        # lee el SHP a limpiar
        if input_path.endswith('.shp'):
            self.df = gpd.read_file(
                input_path,
                encoding=default_args['encoding']
            )
            # lee la proyección del .prj, si puede
            try:
                projection_path = input_path.replace('.shp', '.prj')
                self.source_crs = pycrs.loader.from_file(
                    projection_path).to_proj4()
            except Exception as e:
                print(e)
                self.source_crs = self.df.crs

        # lee el CSV a limpiar
        elif input_path.endswith('.csv'):
            self.df = pd.read_csv(
                input_path, dtype=str, **default_args)

        # lee el XLSX a limpiar
        elif input_path.endswith('.xlsx'):
            self.df = pd.read_excel(input_path, engine="xlrd", **default_args)

        else:
            raise Exception(
                "{} no es un formato soportado.".format(
                    input_path.split(".")[-1]))

        # limpieza automática
        # normaliza los nombres de los campos
        self.df.columns = self._normalize_fields(self.df.columns)

        # remueve todos los saltos de línea
        if len(self.df) > 0:
            self.df = self.df.applymap(self._remove_line_breaks)

        # guarda PEGs compiladas para optimizar performance
        self.grammars = {}

    def _assert_no_duplicates(self, input_path, encoding, sep, quotechar):

        if input_path.endswith('.csv'):
            with open(input_path, 'rb') as csvfile:
                reader = unicodecsv.reader(csvfile,
                                           encoding=encoding,
                                           delimiter=sep,
                                           quotechar=quotechar)
                fields = next(reader, [])

                for col in fields:
                    if fields.count(col) > 1:
                        raise DuplicatedField(col)

        # TODO: Implementar chequeo de que no hay duplicados para XLSX
        elif input_path.endswith('.xlsx'):
            pass

    def _get_file_encoding(self, file_path):
        """Detecta la codificación de un archivo con cierto nivel de confianza
           y devuelve esta codificación o el valor por defecto.

        Args:
            file_path (str): Ruta del archivo.

        Returns:
            str: Codificación del archivo.
        """
        with open(file_path, 'rb') as f:
            info = cchardet.detect(f.read())
        return (info['encoding'] if info['confidence'] > 0.75
                else self.INPUT_DEFAULT_ENCODING)

    def _normalize_fields(self, fields):
        return [self._normalize_field(field) for field in fields]

    def _normalize_field(self, field, sep="_"):
        """Normaliza un string para ser nombre de campo o sufijo de dataset.

        Args:
            field (str): Nombre original del campo o sufijo de datset.
            sep (str): Separador para el nombre normalizado.

        Returns:
            str: Nombre de campo o sufijo de datset normalizado.
        """
        if not isinstance(field, str):
            field = str(field)

        # reemplaza caracteres que no sean unicode
        norm_field = unidecode(field).strip()

        norm_field = norm_field.replace(" ", sep)
        norm_field = norm_field.replace("-", sep).replace("_", sep)
        norm_field = norm_field.replace("/", sep)
        norm_field = self._camel_convert(norm_field).lower()

        # remueve caracteres que no sean alfanuméricos o "_"
        norm_field = ''.join(char for char in norm_field
                             if char.isalnum() or char == "_")

        # emite un Warning si tuvo que normalizar el field
        if field != norm_field:
            caller_rule = self._get_normalize_field_caller(
                inspect.currentframe())
            msg = """

El campo "{}" no sigue las convenciones para escribir
campos (sólo se admiten caracteres alfanuméricos ASCII en
minúsculas, con palabras separadas por "{}"). DataCleaner
normaliza automáticamente los campos en estos casos, lo
que puede llevar a resultados inesperados.

El nuevo nombre del campo normalizado es: "{}".
Método que llamó al normalizador de campos: {}
""".format(field, sep, norm_field, caller_rule)
            warnings.warn(msg)

        return norm_field

    @staticmethod
    def _camel_convert(name):
        return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name)

    @staticmethod
    def _get_normalize_field_caller(curframe):
        curframe = inspect.currentframe()
        calframe = inspect.getouterframes(curframe, 2)

        if calframe[2][3] != "_normalize_fields":
            caller_rule = calframe[2][3]
        else:
            caller_rule = calframe[3][3]

        return caller_rule

    @staticmethod
    def _remove_line_breaks(value, replace_char=" "):
        if isinstance(value, str):
            return str(value).replace('\n', replace_char)
        else:
            return value

    # Métodos GLOBALES
    def clean(self, rules):
        """Aplica las reglas de limpieza al objeto en memoria.

        Args:
            rules (list): Lista de reglas de limpieza.
        """
        for rule_item in rules:
            for rule in rule_item:
                rule_method = getattr(self, rule)
                for kwargs in rule_item[rule]:
                    kwargs["inplace"] = True
                    rule_method(**kwargs)

    def clean_file(self, rules, output_path):
        """Aplica las reglas de limpieza y guarda los datos en un csv.

        Args:
            rules (list): Lista de reglas de limpieza.
        """
        self.clean(rules)
        self.save(output_path)

    def save(self, output_path, geometry_name='geojson',
             geometry_crs='epsg:4326'):
        """Guarda los datos en un nuevo CSV con formato estándar.

        El CSV se guarda codificado en UTF-8, separado con "," y usando '"'
        comillas dobles como caracter de enclosing."""

        if isinstance(self.df, gpd.GeoDataFrame):
            # Convierte la proyección, si puede.
            if geometry_crs:
                try:
                    self.df.crs = self.source_crs
                    self.df = self.df.to_crs({'init': geometry_crs})
                except Exception as e:
                    print(e)
                    print("Se procede sin re-proyectar las coordenadas.")

            if output_path.endswith('.csv'):
                self._set_json_geometry(geometry_name)

            # Guarda el archivo en formato GeoJSON o KML.
            if output_path.endswith('json'):  # Acepta .json y .geojson.
                self.df.to_file(output_path, driver='GeoJSON')
                return
            elif output_path.endswith('kml'):
                self._save_to_kml(output_path)
                return
        self.df.set_index(self.df.columns[0]).to_csv(
            output_path, encoding=self.OUTPUT_ENCODING,
            sep=self.OUTPUT_SEPARATOR,
            quotechar=self.OUTPUT_QUOTECHAR)

    def _save_to_kml(self, output_path):
        aux_file = output_path + '.json'
        self.df.to_file(aux_file, driver='GeoJSON')
        command = 'ogr2ogr -f KML {} {}'.format(output_path, aux_file)
        subprocess.call(command, shell=True)
        os.remove(aux_file)

    def _set_json_geometry(self, geometry_name):
        """Transforma la geometría del GeoDataFrame a formato JSON."""
        geojson = self.df.geometry.to_json()
        features = json.loads(geojson)['features']
        geometries = [feature['geometry'] for feature in features]
        # Convierte cada geometría en un string JSON válido.
        self.df[geometry_name] = [json.dumps(geometry)
                                  for geometry in geometries]
        del self.df['geometry']

    def _update_series(self, field, new_series,
                       keep_original=False, prefix=None, sufix=None):
        """Agrega o pisa una serie nueva en el DataFrame."""
        if not keep_original:
            self.df[field] = new_series
        else:
            new_field = "_".join([elem for elem in [prefix, field, sufix]
                                  if elem])
            self.df.insert(self.df.columns.get_loc(field),
                           new_field, new_series)

    # Métodos INDIVIDUALES de LIMPIEZA
    def remover_columnas(self, field, inplace=False):
        """Remueve columnas.

        Args:
            field (str): Campo a limpiar

        Returns:
            pandas.DataFrame: Data frame con las columnas removidas.
        """
        field = self._normalize_field(field)
        if field not in self.df.columns:
            warnings.warn("No existe el campo '{}'".format(field))
            return self.df
        removed_df = self.df.drop(field, axis=1)

        if inplace:
            self.df = removed_df

        return removed_df

    def remover_filas_duplicadas(self, all_fields=True, fields=None, inplace=False):
        """Remueve filas duplicadas.

        Args:
            all_fields (bool): Si es true, se usan todas las columnas y se ignora el argumento fields
            fields (list): Lista de nombres de columnas a ser usadas para identificar filas duplicadas
            inplace (bool): Específica si la limpieza perdura en el objeto.

        Returns:
            pandas.DataFrame: Data frame con las columnas removidas.
        """
        if all_fields:
            removed_df = self.df.drop_duplicates().reset_index(drop=True)
        else:
            removed_df = self.df.drop_duplicates(
                subset=fields).reset_index(drop=True)

        if inplace:
            self.df = removed_df

        return removed_df

    def renombrar_columnas(self, field, new_field, inplace=False):
        """Renombra una columna.

        Args:
            field (str): Campo a renombrar.
            field (str): Nuevo nombre

        Returns:
            pandas.DataFrame: Data frame con las columnas renombradas.
        """
        field = self._normalize_field(field)
        new_field = self._normalize_field(new_field)
        renamed_df = self.df.rename(columns={field: new_field})

        if inplace:
            self.df = renamed_df

        return renamed_df

    def nombre_propio(self, field, sufix=None, lower_words=None,
                      keep_original=False, inplace=False):
        """Regla para todos los nombres propios.

        Capitaliza los nombres de países, ciudades, personas, instituciones y
        similares.

        Args:
            field (str): Campo a limpiar

        Returns:
            pandas.Series: Serie de strings limpios
        """
        sufix = sufix or self.DEFAULT_SUFIX
        field = self._normalize_field(field)
        series = self.df[field]
        capitalized = series.apply(capitalize, lower_words=lower_words)

        if inplace:
            self._update_series(field=field, sufix=sufix,
                                keep_original=keep_original,
                                new_series=capitalized)
        return capitalized

    def string(self, field, sufix=None, sort_tokens=False,
               remove_duplicates=False, keep_original=False, inplace=False):
        """Regla para todos los strings.

        Aplica un algoritimo de clustering para normalizar strings que son
        demasiado parecidos, sin pérdida de información.

        Args:
            field (str): Campo a limpiar.

        Returns:
            pandas.Series: Serie de strings limpios.
        """
        sufix = sufix or self.DEFAULT_SUFIX
        field = self._normalize_field(field)
        series = self.df[field]

        clusters, counts = group_fingerprint_strings(
            series, sort_tokens=sort_tokens,
            remove_duplicates=remove_duplicates)
        replacements = get_best_replacements(clusters, counts)
        parsed_series = pd.Series(replace_by_key(replacements, series))
        parsed_series = parsed_series.str.strip()

        if inplace:
            self._update_series(field=field, sufix=sufix,
                                keep_original=keep_original,
                                new_series=parsed_series)

        return parsed_series

    def mail_format(self, field, sufix=None,
                    keep_original=False, inplace=False):
        """Regla para dar formato a las direcciones de correo electronico.

        Lleva todas las cadenas a minusculas y luego si hay varias las separa
        por comas.

        Args:
            field (str): Campo a limpiar

        Returns:
            pandas.Series: Serie de strings limpios
        """
        sufix = sufix or self.DEFAULT_SUFIX
        field = self._normalize_field(field)
        series = self.df[field].str.lower()
        series = series.str.findall('[a-z_0-9\.]+@[a-z_0-9\.]+').str.join(", ")

        if inplace:
            self._update_series(field=field, sufix=sufix,
                                keep_original=keep_original,
                                new_series=series)

        return series

    def reemplazar(self, field, replacements, sufix=None,
                   keep_original=False, inplace=False):
        """Reemplaza listas de valores por un nuevo valor.

        Args:
            field (str): Campo a limpiar
            replacements (dict): {"new_value": ["old_value1", "old_value2"]}

        Returns:
            pandas.Series: Serie de strings limpios
        """
        sufix = sufix or self.DEFAULT_SUFIX
        field = self._normalize_field(field)
        series = self.df[field]

        for new_value, old_values in replacements.items():
            series = series.replace(old_values, new_value)

        if inplace:
            self._update_series(field=field, sufix=sufix,
                                keep_original=keep_original,
                                new_series=series)

        return series

    def reemplazar_string(self, field, replacements, sufix=None,
                          keep_original=False, inplace=False):
        """Reemplaza listas de strings por un nuevo string.
           A diferencias de la funcion reemplazar hace reemplazos parciales.

        Args:
            field (str): Campo a limpiar
            replacements (dict): {"new_value": ["old_value1", "old_value2"]}

        Returns:
            pandas.Series: Serie de strings limpios
        """
        sufix = sufix or self.DEFAULT_SUFIX
        field = self._normalize_field(field)
        series = self.df[field]

        for new_value, old_values in replacements.items():
            # for old_value in sorted(old_values, key=len, reverse=True):
            for old_value in old_values:
                replace_function = partial(self._safe_replace,
                                           old_value=old_value,
                                           new_value=new_value)
                series = map(replace_function, series)

        if inplace:
            self._update_series(field=field, sufix=sufix,
                                keep_original=keep_original,
                                new_series=series)

        return series

    @staticmethod
    def _safe_replace(string, old_value, new_value):
        if pd.isnull(string):
            return pd.np.nan
        else:
            return str(string).replace(old_value, new_value)

    def fecha_completa(self, field, time_format, keep_original=False,
                       inplace=False):
        """Regla para fechas completas que están en un sólo campo.

        Args:
            field (str): Campo a limpiar.
            time_format (str): Formato temporal del campo.

        Returns:
            pandas.Series: Serie de strings limpios
        """
        field = self._normalize_field(field)
        series = self.df[field]
        parsed_series = series.apply(self._parse_datetime,
                                     args=(time_format,))
        if inplace:
            self._update_series(field=field, prefix="isodatetime",
                                keep_original=keep_original,
                                new_series=parsed_series)

        return parsed_series

    def fecha_simple(self, field, time_format, keep_original=False,
                     inplace=False):
        """Regla para fechas sin hora, sin día o sin mes.

        Args:
            field (str): Campo a limpiar.
            time_format (str): Formato temporal del campo.

        Returns:
            pandas.Series: Serie de strings limpios
        """
        field = self._normalize_field(field)
        series = self.df[field]
        parsed_series = series.apply(self._parse_date,
                                     args=(time_format,))

        if inplace:
            self._update_series(field=field, prefix="isodate",
                                keep_original=keep_original,
                                new_series=parsed_series)

        return parsed_series

    @staticmethod
    def _parse_datetime(value, time_format):
        try:
            datetime = arrow.get(
                value, time_format,
                tzinfo=tz.gettz("America/Argentina/Buenos Aires"))
            print(value, time_format, datetime, "funciona")
            return datetime.isoformat()
        except:
            print(value, time_format, "no funciona")
            return ""

    @staticmethod
    def _parse_date(value, time_format):
        try:
            datetime = arrow.get(
                value, time_format,
                tzinfo=tz.gettz("America/Argentina/Buenos Aires"))
            date = datetime.isoformat().split("T")[0]

            if "D" in time_format:
                return date
            elif "M" in time_format:
                return "-".join(date.split("-")[:-1])
            else:
                return "-".join(date.split("-")[:-2])
        except:
            return ""

    def fecha_separada(self, fields, new_field_name, keep_original=True,
                       inplace=False):
        """Regla para fechas completas que están separadas en varios campos.

        Args:
            field (str): Campo a limpiar.
            new_field_name (str): Sufijo para construir nombre del nuevo field.

        Returns:
            pandas.Series: Serie de strings limpios.
        """
        field_names = [self._normalize_field(field[0]) for field in fields]
        time_format = " ".join([field[1] for field in fields])

        # print(time_format)
        # print(self.df[field_names])
        concat_series = self.df[field_names].apply(
            lambda x: ' '.join(x.map(str)),
            axis=1
        )
        print(concat_series)

        parsed_series = concat_series.apply(self._parse_datetime,
                                            args=(time_format,))
        print(parsed_series)

        if inplace:
            self.df["isodatetime_" + new_field_name] = parsed_series
            if not keep_original:
                for field in field_names:
                    self.remover_columnas(field)

        return parsed_series

    def string_simple_split(self, field, separators, new_field_names,
                            keep_original=True, inplace=False):
        """Regla para separar un campo a partir de separadores simples.

        Args:
            field (str): Campo a limpiar
            separators (list): Strings separadores.
            new_field_names (list): Sufijos de los nuevos campos para los
                valores separados.

        Returns:
            pandas.Series: Serie de strings limpios
        """
        field = self._normalize_field(field)
        series = self.df[field]
        parsed_df = series.apply(self._split, args=(separators,))

        parsed_df.rename(
            columns={key: field + "_" + value
                     for key, value in enumerate(new_field_names)},
            inplace=True
        )

        if inplace:
            self.df = pd.concat([self.df, parsed_df], axis=1)
        if not keep_original:
            self.remover_columnas(field)

        return parsed_df

    @staticmethod
    def _split(value, separators):
        values = []
        for separator in separators:
            if separator in str(value):
                values = [str(split_value) for split_value in
                          value.split(separator)]
                break

        return pd.Series([str(value).strip() for value in values
                          if pd.notnull(value)])

    def string_regex_split(self, field, pattern, new_field_names,
                           keep_original=True, inplace=False):
        """Regla para separar un campo a partir de una expresión regular.

        TODO!!! Falta implementar este método.

        Args:
            field (str): Campo a limpiar.
            pattern (str): Expresión regular.
            new_field_names (list): Sufijos de los nuevos campos para los
                valores separados.

        Returns:
            pandas.Series: Serie de strings limpios
        """
        field = self._normalize_field(field)
        pass

    def string_peg_split(self, field, grammar, new_field_names,
                         keep_original=True, inplace=False):
        """Regla para separar un campo a partir parsing expression grammars.

        Args:
            field (str): Campo a limpiar.
            grammar (str): Reglas para compilar una PEG.
            new_field_names (list): Sufijos de los nuevos campos para los
                valores separados.

        Returns:
            pandas.Series: Serie de strings limpios
        """
        field = self._normalize_field(field)
        series = self.df[field]
        parsed_df = series.apply(self._split_with_peg, args=(grammar,))

        parsed_df.rename(
            columns={key: field + "_" + value
                     for key, value in enumerate(new_field_names)},
            inplace=True
        )

        if inplace:
            self.df = pd.concat([self.df, parsed_df], axis=1)
        if not keep_original:
            self.remover_columnas(field)

        return parsed_df

    def _split_with_peg(self, value, grammar):
        if grammar in self.grammars:
            comp_grammar = self.grammars[grammar]
        else:
            comp_grammar = parsley.makeGrammar(grammar, {})
            self.grammars[grammar] = comp_grammar

        try:
            values = comp_grammar(value).values()
        except:
            values = []

        values = [str(split_value) for split_value in values]

        return pd.Series(values)

    def string_regex_substitute(self, field, regex_str_match,
                                regex_str_sub, sufix=None,
                                keep_original=True, inplace=False):
        """Regla para manipular y reeemplazar datos de un campo con regex.

        Args:
            field (str): Campo a limpiar.
            regex_str_match (str): Expresion regular a buscar
            regex_str_sub (str): Expresion regular para el reemplazo.

        Returns:
            pandas.Series: Serie de strings limpios
        """
        sufix = sufix or self.DEFAULT_SUFIX
        field = self._normalize_field(field)
        series = self.df[field]
        replaced = series.str.replace(regex_str_match, regex_str_sub)

        if inplace:
            self._update_series(field=field, sufix=sufix,
                                keep_original=keep_original,
                                new_series=replaced)

        return replaced

    def simplificar_geometria(self, tolerance=0.5,
                              keep_original=True, inplace=False):
        """Simplifica una geometría para que resulte
           en un objeto de menor tamaño y complejidad,
           que a la vez retenga sus características esenciales.

        Args:
            tolerance (float): Nivel de tolerancia en la transformación.

        Returns:
            pandas.Series: Serie de geometrías.
        """
        if isinstance(self.df, gpd.GeoDataFrame):
            self.df.geometry = self.df.geometry.simplify(tolerance)
            return self.df.geometry
        else:
            raise TypeError('El dataframe no es de tipo GeoDataFrame.')

    def normalizar_unidad_territorial(self, field, entity_level, add_code=False,
                                      add_centroid=False, add_parents=None,
                                      filters=None, keep_original=False,
                                      inplace=False):
        """Normaliza y enriquece una unidad territorial del DataFrame.

        Args:
            field (str): Nombre del campo a normalizar.
            entity_level (str): Nivel de la unidad territorial.
            add_code (bool): Específica si agrega código de la entidad.
            add_centroid (bool): Específica si agrega centroide de la entidad.
            add_parents (list): Lista de entidades padres a agregar.
            filters (dict): Diccionario con entidades por las cuales filtrar.
            keep_original (bool): Específica si conserva la columna original.
            inplace (bool): Específica si la limpieza perdura en el objeto.

        Returns:
            pandas.Series: Serie de unidades territoriales normalizadas y
                limpias.
        """
        if len(self.df) > 100000:
            print('El número máximo de unidades a normalizar es de 100000.')
            return

        if not self._validate_entity_level(entity_level):
            print('"{}" no es un nivel de entidad válido.'.format(entity_level))
            return

        if filters:
            if not self._validate_filters(entity_level, filters):
                return self.df

        data = self._build_data(field, entity_level, filters)
        if data:
            res = self._get_api_response(entity_level, data)

            if 'error' in res:
                print(res['error'])

            if keep_original:
                field_normalized = str(field + '_normalized')
                self._update_column(field_normalized, NAME, entity_level, res)
            else:
                self._update_column(field, NAME, entity_level, res)

            if add_code:
                column_code = entity_level + '_' + ID
                self._update_column(column_code, ID, entity_level, res)

            if add_centroid:
                column_lat = entity_level + '_' + LAT
                column_lon = entity_level + '_' + LON
                self._update_column(column_lat, LAT, entity_level, res)
                self._update_column(column_lon, LON, entity_level, res)

            if add_parents:
                for parent in add_parents:
                    if entity_level not in PROV and parent in PROV:
                        self._update_column(
                            PROV_ID, PROV_ID, entity_level, res)
                        self._update_column(PROV_NAM, PROV_NAM, entity_level,
                                            res)
                    if parent in DEPT and entity_level in [MUN, LOC]:
                        self._update_column(
                            DEPT_ID, DEPT_ID, entity_level, res)
                        self._update_column(DEPT_NAM, DEPT_NAM, entity_level,
                                            res)
                    if parent in MUN and entity_level in LOC:
                        self._update_column(MUN_ID, MUN_ID, entity_level, res)
                        self._update_column(
                            MUN_NAM, MUN_NAM, entity_level, res)

            return self.df
        else:
            return

    @staticmethod
    def _validate_filters(entity_level, filters):
        """Verifica que los filtros sean validos con el nivel de entidad a
        normalizar.

        Args:
            entity_level: Nivel de la unidad territorial por la cual filtrar.
            filters (dict): Diccionario con filtros.

        Returns:
            bool: Verdadero si los filtros son válidos.
        """

        field_prov = PROV + '_field'
        field_dept = DEPT + '_field'
        field_mun = MUN + '_field'

        # Verfica que se utilicen keywords válidos por entidad
        for key, value in filters.items():

            if key not in [field_prov, field_dept, field_mun]:
                print('"{}" no es un keyword válido.'.format(key))
                return
            if entity_level in key or entity_level in PROV:
                print('"{}" no es un filtro válido para la entidad "{}."'
                      .format(key, entity_level))
                return
            if entity_level in DEPT and PROV not in key:
                print('"{}" no es un filtro válido para la entidad "{}".'
                      .format(key, entity_level))
                return

        return True

    def _build_data(self, field, entity_level, filters):
        """Construye un diccionario con una lista de unidades territoriales
        para realizar consultas a la API de normalización Georef utilizando
        el método bulk.

        Args:
            field (str): Nombre del campo a normalizar.
            entity_level (str): Nivel de la unidad territorial.
            filters (dict): Diccionario con entidades por las cuales filtrar.

        Returns:
            dict: (dict): Diccionario a utilizar para realizar una consulta.
            En caso de error devuelve False.
        """
        body = []
        entity_level = self._plural_entity_level(entity_level)

        try:
            for item, row in self.df.iterrows():
                row = row.fillna('0')  # reemplaza valores 'nan' por '0'
                data = {'nombre': row[field], 'max': 1, 'aplanar': True}

                if filters:
                    filters_builded = self._build_filters(row, filters)
                    data.update(filters_builded)
                body.append(data)

            return {entity_level: body}
        except KeyError as e:
            print('Error: No existe el campo "{}".'.format(e))
        except Exception as e:
            print(e)
        return False

    @staticmethod
    def _build_filters(row, filters):
        """Contruye un diccionario con filtros de unidades territoriales.

        Args:
            row (pandas.Series): Serie con strings de unidades territoriales.
            filters (dict): Diccionario con filtros.

        Returns:
            params (dict): Diccionario con filtros.
        """

        params = {}
        field_prov = PROV + '_field'
        field_dept = DEPT + '_field'
        field_mun = MUN + '_field'
        row = row.fillna(0)

        # Si existe el filtro y su valor no es 0 lo agrega al diccionario
        if field_prov in filters and row[filters[field_prov]]:
            params.update({PROV: row[filters[field_prov]]})
        if field_dept in filters and row[filters[field_dept]]:
            params.update({DEPT: row[filters[field_dept]]})
        if field_mun in filters and row[filters[field_mun]]:
            params.update({MUN: row[filters[field_mun]]})
        return params

    @staticmethod
    def _get_api_response(entity_level, data):
        """Realiza búsquedas sobre un listado de entidades en simultáneo
        utilizando el método bulk de la API de normalización Georef.

        Args:
            entity_level (str): Nivel de la unidad territorial a consultar.
            data (dict): Diccionario que contiene un listado de unidades
                territoriales.

        Returns:
            results (list): Lista con resultados de la búsqueda.
        """
        wrapper = GeorefWrapper()
        if entity_level in PROV:
            results = wrapper.search_province(data)
        elif entity_level in DEPT:
            results = wrapper.search_departament(data)
        elif entity_level in MUN:
            results = wrapper.search_municipality(data)
        else:
            results = wrapper.search_locality(data)
        return results

    def _update_column(self, column, attribute, entity_level, results):
        """Actualiza una columna específica del DataFrame.

        Args:
            column (str): Nombre de la columna a agregar y/o actualizar.
            attribute (str): Nombre del atributo del
            entity_level (str): Nivel de la unidad territorial a consultar.
            results (list): Resultado de la consulta a la API.

        Return:
            None
        """
        entity_level = self._plural_entity_level(entity_level)

        if column not in self.df:
            self.df[column] = None

        idx = 0
        for row in results:
            if row[entity_level]:
                self.df.loc[idx, column] = row[entity_level][0][attribute]
            idx += 1

    @staticmethod
    def _plural_entity_level(entity_level):
        """Pluraliza el nombre de una unidad territorial.

        Args:
            entity_level (str): Nivel de la unidad territorial a pluralizar.

        Return:
            entity_level (str): Nombre pluralizado.
        """
        if LOC not in entity_level:
            entity_level = entity_level + 's'
        else:
            entity_level = entity_level + 'es'
        return entity_level

    @staticmethod
    def _validate_entity_level(entity_level):
        """Válida el nivel de la unidad territorial.

        Args:
            entity_level (str): Nivel de la unidad territorial a validar.

        Return:
            bool: Verdadero si es un nivel de entidad válida.
        """
        if entity_level in [PROV, DEPT, MUN, LOC]:
            return True
        return False