#!/usr/bin/env python
# encoding: utf-8
#
#  ------------------------------------------------------------------------------
#  Name: run.py
#  Version: 0.0.1
#  Summary: Bacen IF.data AutoScraper & Data Manager
#           Este sistema foi projetado para automatizar o download dos
#           relatórios da ferramenta IF.data do Banco Central do Brasil.
#           Criado para facilitar a integração com ferramentas automatizadas de
#           análise e visualização de dados, garantido acesso fácil e oportuno
#           aos dados.
#
#  Author: Alexsander Lopes Camargos
#  Author-email: alcamargos@vivaldi.net
#
#  License: MIT
#  ------------------------------------------------------------------------------

"""Bacen IF.data AutoScraper & Data Manager"""

from enum import StrEnum
from pathlib import Path

from loguru import logger

from bacen_ifdata.scraper.institutions import InstitutionType as Institutions
from bacen_ifdata.scraper.storage.processing import build_directory_path
from bacen_ifdata.utilities.configurations import Config as Cfg


def check_file_already_processed(output_directory: Path, file: str) -> bool:
    """Checks if the file has already been processed.

    Args:
        output_directory (Path): The directory where the file is being processed.
        file (str): The name of the file to be processed.

    Returns:
        bool: True if the file has already been processed, False otherwise.
    """

    # Check if the file exists in the output directory.
    return (output_directory / file).exists()


def _find_data_start_index(data: list[str]) -> int:
    """Determines the start index of the data rows.

    Args:
        data (list[str]): The raw lines of the CSV file.

    Returns:
        int: The index where the data rows begin.
    """

    start = 1

    for index in range(1, 6):
        if index >= len(data):
            break
        current_line = data[index].split(';')
        if current_line[0] == '':
            start += 1
        else:
            break

    return start


def _fill_missing_header_values(header: list[str], data: list[str], start: int) -> list[str]:
    """Fills empty header cells using values from subsequent header rows.

    Args:
        header (list[str]): The current header list to be updated.
        data (list[str]): The raw lines of the CSV file.
        start (int): The start index of the data rows.

    Returns:
        list[str]: The updated header list.
    """

    # Create a new header list to store the processed header values.
    processed_header = header.copy()

    for column_index, column_value in enumerate(processed_header):
        if column_value == "":
            for row_index in range(start - 1, 0, -1):
                cols = data[row_index].split(";")
                if len(cols) > column_index and cols[column_index].strip():
                    processed_header[column_index] = cols[column_index].strip()
                    break

    return processed_header


def _apply_header_grouping(header: list[str], original_line0: list[str]) -> list[str]:
    """Applies group names to sub-columns.

    Args:
        header (list[str]): The current header list to be updated.
        original_line0 (list[str]): The original first line of the CSV for group detection.

    Returns:
        list[str]: The updated header list.
    """

    # Create a new header list to store the processed header values.
    processed_header = header.copy()

    current_group = None
    for column_index, column_header in enumerate(processed_header):
        original_value = original_line0[column_index].strip() if column_index < len(original_line0) else ""

        if original_value:
            # This column had a value on line 0 — it's either a
            # standalone column (e.g., "Instituição") or a group header.
            next_is_empty = column_index + 1 < len(original_line0) and not original_line0[column_index + 1].strip()

            if next_is_empty:
                current_group = original_value
            else:
                current_group = None
        elif current_group and column_header.strip():
            # This column was empty on line 0 — it's a sub-column.
            # Prepend the group name for a composite header.
            processed_header[column_index] = f'{current_group} - {column_header}'

    return processed_header


def _process_csv_header(data: list[str]) -> tuple[list[str], int]:
    """Processes the CSV header to handle groupings and inconsistencies.

    Args:
        data (list[str]): The raw lines of the CSV file.
    Returns:
        tuple[list[str], int]: A tuple containing the processed header list and the start index of data rows.
    """

    header = data[0].split(';')
    start = _find_data_start_index(data)

    # If the data starts after the first line, it indicates that there are multiple header lines due to grouping.
    if start > 1:
        # Save original line 0 to know which columns had group names
        original_line0 = data[0].split(';')
        # Fill missing header values using subsequent header rows and apply group names to sub-columns.
        header = _fill_missing_header_values(header, data, start)
        # Apply group names to sub-columns based on the original line 0.
        header = _apply_header_grouping(header, original_line0)

    return header, start


# pylint: disable=too-many-locals
def normalize_csv(institution: Institutions, report: StrEnum, file: str) -> bool:
    """Normalizes a CSV file from Bacen If.Data by correcting its header
    structure and removing inconsistent lines at both the start and end
    of the file.

    The normalization process involves several steps:
    1. Correcting header inconsistencies due to non-standard CSV formatting by Bacen,
       where column titles might appear in different lines based on header groupings.
    2. Removing any empty final column in the header.
    3. Discarding additional lines at the end of the file that contain consolidated
       report information and do not conform to the standard data format.

    The function checks if the file has already been normalized before proceeding.

    Args:
        institution (Institutions): An enumerated value representing the institution.
        report (StrEnum): An enumerated value representing the report type.
        file (str): The name of the file to be normalized.

    Returns:
        bool: True if the file is successfully normalized, False otherwise.

    Raises:
        FileNotFoundError: If the specified file is not found in the input directory.
        IOError: If an input/output error occurs during file processing.

    Note:
        The function assumes the presence of a specific directory structure
        for input (downloaded CSV files) and output (normalized CSV files),
        as defined in the configuration module (`CONFIG`).
    """

    # Directory where the raw CSV files are stored after being downloaded.
    input_path = build_directory_path(Cfg.DOWNLOAD_DIRECTORY, institution.name.lower(), report.name.lower())

    # Directory where the normalized CSV files will be stored.
    output_path = build_directory_path(Cfg.PROCESSED_FILES_DIRECTORY, institution.name.lower(), report.name.lower())

    # Check if the file has already been normalized.
    if check_file_already_processed(output_path, file):
        logger.info(f'File {file} has already been normalized, skipping...')
        return False

    # Normalize the file.
    try:
        with (
            # Note: 'utf-8-sig' is used to handle potential BOM in the input CSV files.
            open(f'{input_path}\\{file}', 'r', encoding='utf-8-sig') as input_file,
            open(f'{output_path}\\{file}', 'w', encoding='utf-8') as output_file,
        ):
            data = input_file.readlines()

            # The developers responsible for the Bacen website deviated from
            # the standard CSV format. They implemented an unusual approach
            # by including groups of headers within the CSV file.
            # This results in an inconsistent structure where the column
            # title may appear in either the first or third line, varying
            # according to the number of groupings in the headers.
            # This peculiarity presents a unique challenge in handling these files.
            header, start = _process_csv_header(data)

            # On certain occasions, the CSV files provided by Bacen
            # feature a final column with no data in the header.
            # To ensure the integrity and consistency of the data,
            # this empty column will be identified and removed.
            if header[-1] in ('\n', ''):
                # Remove the last column.
                header.pop()

            # Remove the inconsistent header.
            data = data[start:]

            # The CSV files provided by Bacen include additional lines
            # at the end containing consolidated report information.
            # These extra lines do not conform to the standard format
            # of the rest of the file and thus need to be removed for
            # an accurate data analysis. The approach involves identifying
            # and discarding all lines following the first one that does
            # not have the same number of columns as the header.
            mismatch_index = next(
                (index for index, line in enumerate(data) if len(line.rstrip().split(';')) != len(header)), len(data)
            )
            data = data[:mismatch_index]

            # Reconstruct the header line with the corrected structure.
            header_line = ';'.join(header) + '\n'
            output_file.write(header_line)  # Write the corrected header line to the output file.
            output_file.writelines(data)  # Write the remaining data lines to the output file without modification.

        # Return True if the file was successfully normalized.
        return True

    # Handle exceptions.
    except FileNotFoundError as error:
        logger.error(f'File not found: {error.filename}')
        return False
    except IOError as error:
        logger.error(f'Input/output error: {error}')
        return False
