# =============================================================================
# MS_ANALYZER_GENERAL_V2.1.py
#
# Version STREAMING:
# - No almacena la matriz completa en memoria.
# - Lee el archivo CSV fila por fila.
# - Analiza cada elemento durante la lectura.
# =============================================================================


# =============================================================================
# 1 IMPORTS
# =============================================================================

import csv
import time

from sympy import isprime
import re

# =============================================================================
# 2 CONFIGURACION
# =============================================================================

# Tamaño de segmento para la criba segmentada

SEGMENT_SIZE = 5_000_000

# Dimensión máxima permitida de la matriz

MAX_MAGIC_SIZE = 9999

# =============================================================================
# 2.1 PARAMETROS DE LA MATRIZ
# =============================================================================

# Estos valores serán definidos dinámicamente
# a partir del nombre del archivo de entrada

CSV_FILE = ""
REPORT_FILE = ""
MAGIC_SIZE = 0
ONLY_PRIMES = False

# =============================================================================
# 3 FUNCIONES AUXILIARES
# =============================================================================


def elapsed(start_time):

    return time.perf_counter() - start_time



def stop_program(message, start_time):
    """
    Reporta un error y termina el proceso.
    """

    total_time = elapsed(start_time)

    print()
    print(message)
    print()
    print(
        f"Proceso interrumpido. Tiempo total: "
        f"{total_time:.2f} segundos"
    )

    write_report([

        "MS_MAGIC_ANALYZER_Angelone_V1.3",
        "",
        "RESULTADO:",
        message,
        "",
        f"Tiempo total: {total_time:.2f} segundos"

    ])



def write_report(lines):
    """
    Escribe el reporte final en archivo de texto.
    """

    with open(
        REPORT_FILE,
        "w",
        encoding="utf-8"
    ) as file:

        for line in lines:
            file.write(line + "\n")


def get_matrix_parameters():
    """
    Solicita el archivo de entrada y obtiene los parámetros de la matriz.
    Formato esperado: MM<N>.csv
    """

    global CSV_FILE
    global REPORT_FILE
    global MAGIC_SIZE
    global ONLY_PRIMES

    while True:

        print()
        print("La matriz debe estar guardada con formato MMN.csv,")
        print("donde N es un número impar que representa el orden de la matriz")
        print("(3 < N < 9999).")
        print("Ese es el nombre que debe introducir en la siguiente línea.")
        print()

        CSV_FILE = input(
            "Nombre de la matriz CSV: "
        ).strip()


        match = re.fullmatch(
            r"MM(\d+)\.csv",
            CSV_FILE
        )


        if not match:

            print(
                "ERROR: Formato inválido. "
                "Use MM<N>.csv"
            )
            continue


        MAGIC_SIZE = int(
            match.group(1)
        )


        if MAGIC_SIZE > MAX_MAGIC_SIZE:

            print(
                f"ERROR: Dimensión máxima permitida: "
                f"{MAX_MAGIC_SIZE}"
            )

            continue


        break


    REPORT_FILE = (
        CSV_FILE.replace(
            ".csv",
            "_REPORT.txt"
        )
    )


    while True:

        print()

        print("Tipo de matriz:")

        print("1 = Contiene únicamente números primos")

        print("2 = Contiene números primos y/o compuestos")

        option = input(
            "Seleccione (1/2): "
        ).strip()


        if option == "1":

            ONLY_PRIMES = True
            break


        if option == "2":

            ONLY_PRIMES = False
            break


        print()

        print(
            "ERROR: Seleccione únicamente 1 o 2."
        )


    return (
        CSV_FILE,
        REPORT_FILE,
        MAGIC_SIZE,
        ONLY_PRIMES
    )
            
# =============================================================================
# 4 ANALISIS STREAMING DE LA MATRIZ
# =============================================================================


def analyze_streaming(start_time):
    """
    Lee y analiza la matriz directamente desde el archivo CSV.

    No almacena la matriz completa en memoria.

    Verifica:
        - Estructura cuadrada.
        - Primalidad.
        - Unicidad.
        - Magicidad.

    Retorna los resultados necesarios para el reporte final.
    """


    print("Leyendo y analizando archivo CSV...")


    try:

        with open(
            CSV_FILE,
            newline="",
            encoding="utf-8"
        ) as file:


            reader = csv.reader(file)


            n = None
            row_count = 0

            column_sums = None

            seen = {}

            min_prime = None
            max_prime = None

            center_value = None

            diagonal_1 = 0
            diagonal_2 = 0

            magic_sum = None



            for raw_row in reader:


                # Detectar fila completamente vacía

                if (
                    not raw_row
                    or all(value.strip() == "" for value in raw_row)
                ):

                    break



                # Eliminar columna exterior vacía

                row = [
                    value
                    for value in raw_row[:MAGIC_SIZE]
                    if value.strip() != ""
                ]



                # Primera fila: determinar n

                if n is None:

                    n = MAGIC_SIZE

                    if n == 0:

                        stop_program(
                            "ERROR: No se encontraron datos.",
                            start_time
                        )

                        return None


                    column_sums = [0] * n



                row_count += 1

                # Verificar que no existan filas adicionales

                if len(row) != n:

                    stop_program(
                        f"ERROR: Fila con cantidad incorrecta de elementos.\n"
                        f"Fila: {row_count}\n"
                        f"Elementos encontrados: {len(row)}\n"
                        f"Elementos esperados: {n}",
                        start_time
                    )

                    return None



                # Mostrar actividad periódica

                if row_count % 10 == 0:

                    print(
                        f"\rAnalizando fila {row_count}/{n}",
                        end="",
                        flush=True
                    )



                # Analizar elementos de la fila

                for j, value in enumerate(row):


                    value = int(
                        value.replace(",", "")
                    )



                    # Verificación de primalidad

                    if ONLY_PRIMES:

                        if not isprime(value):
    
                            stop_program(
                                f"ERROR: Valor no primo encontrado.\n"
                                f"Posición: fila {row_count}, columna {j+1}\n"
                                f"Valor: {value}",
                                start_time
                            )

                            return None



                    # Actualización de rango de primos

                    if (
                        min_prime is None
                        or value < min_prime
                    ):

                        min_prime = value


                    if (
                        max_prime is None
                        or value > max_prime
                    ):

                        max_prime = value

                    # Verificación de unicidad

                    if value in seen:

                        old_position = seen[value]

                        stop_program(
                            f"ERROR: Primo repetido encontrado.\n"
                            f"Valor: {value}\n"
                            f"Primera posición: fila {old_position[0]}, columna {old_position[1]}\n"
                            f"Segunda posición: fila {row_count}, columna {j+1}",
                            start_time
                        )

                        return None


                    seen[value] = (
                        row_count,
                        j + 1
                    )



                    # Acumulación de columnas

                    column_sums[j] += value



                    # Captura del centro

                    if (
                        row_count == (n // 2 + 1)
                        and j == (n // 2)
                    ):

                        center_value = value



                    # Acumulación de diagonales

                    if j == (row_count - 1):

                        diagonal_1 += value


                    if j == (n - row_count):

                        diagonal_2 += value    

            # Fin de lectura de la matriz

            if row_count != n:

                stop_program(
                    f"ERROR: Cantidad incorrecta de filas.\n"
                    f"Filas encontradas: {row_count}\n"
                    f"Filas esperadas: {n}",
                    start_time
                )

                return None



            # Suma mágica de referencia

            magic_sum = column_sums[0]



            # Verificación de columnas

            for j in range(n):

                if column_sums[j] != magic_sum:

                    stop_program(
                        f"ERROR: Columna no mágica.\n"
                        f"Columna: {j+1}\n"
                        f"Suma encontrada: {column_sums[j]}\n"
                        f"Suma esperada: {magic_sum}",
                        start_time
                    )

                    return None



            # Verificación de diagonales

            if diagonal_1 != magic_sum:

                stop_program(
                    f"ERROR: Diagonal principal no mágica.\n"
                    f"Suma encontrada: {diagonal_1}\n"
                    f"Suma esperada: {magic_sum}",
                    start_time
                )

                return None



            if diagonal_2 != magic_sum:

                stop_program(
                    f"ERROR: Diagonal secundaria no mágica.\n"
                    f"Suma encontrada: {diagonal_2}\n"
                    f"Suma esperada: {magic_sum}",
                    start_time
                )

                return None



            print()

            print("Análisis streaming completado correctamente.")


            return (
                n,
                magic_sum,
                center_value,
                min_prime,
                max_prime
            )
    except Exception as e:

        stop_program(
            f"ERROR: {e}",
            start_time
        )

        return None

# =============================================================================
# 4.1 VERIFICACION DE PRIMOS CONSECUTIVOS
# =============================================================================

def simple_sieve(limit):

    """
    Genera una lista de números primos hasta 'limit'
    usando la criba clásica de Eratóstenes.
    """

    sieve = [True] * (limit + 1)

    sieve[0] = False
    sieve[1] = False


    p = 2

    while p * p <= limit:

        if sieve[p]:

            for multiple in range(
                p * p,
                limit + 1,
                p
            ):
                sieve[multiple] = False

        p += 1


    primes = [
        number
        for number in range(limit + 1)
        if sieve[number]
    ]

    return primes



def count_primes_segmented(low, high):

    """
    Cuenta la cantidad de números primos en el intervalo cerrado
    [low, high] usando una criba segmentada.
    """

    if high < 2:
        return 0


    # Ajuste del límite inferior

    low = max(low, 2)


    # Generar primos base hasta sqrt(high)

    limit = int(high ** 0.5) + 1

    base_primes = simple_sieve(limit)

    count = 0

    segment_start = low

    segment_number = 0

    total_segments = (
        (high - low) // SEGMENT_SIZE
    ) + 1


    while segment_start <= high:
        segment_number += 1

        print(
            f"\rConsecutividad: segmento {segment_number}/{total_segments}",
            end="",
            flush=True
        )


        segment_end = min(
            segment_start + SEGMENT_SIZE - 1,
            high
        )


        segment_size = (
            segment_end - segment_start + 1
        )


        segment = [True] * segment_size


        for prime in base_primes:


            start = max(
                prime * prime,
                ((segment_start + prime - 1) // prime) * prime
            )


            for multiple in range(
                start,
                segment_end + 1,
                prime
            ):
                segment[
                    multiple - segment_start
                ] = False


        count += sum(segment)


        segment_start = segment_end + 1

    print()


    return count

# =============================================================================
# 5 CALCULOS FINALES Y REPORTE
# =============================================================================


def generate_final_report(
    n,
    magic_sum,
    center_value,
    start_time,
    min_prime,
    max_prime,
    consecutive_primes,
    only_primes
):
    """
    Calcula los resultados finales y genera el reporte.
    """



    # Coordenadas centrales (numeración humana)

    center_position = (
        n // 2 + 1,
        n // 2 + 1
    )



    # Producto N*C

    n_times_c = n * center_value



    # Diferencia

    difference = magic_sum - n_times_c



    # Cantidad de elementos

    total_elements = n * n



    # Rango de primos

    prime_range = max_prime - min_prime



    # Tiempo total

    total_time = elapsed(start_time)



    if difference == 0:

        relation = "SM = N*C"

    else:

        relation = "SM != N*C"

    if only_primes:

        consecutive_text = (
            "SI" if consecutive_primes else "NO"
        )

    else:

        consecutive_text = "NO APLICA"

    if only_primes:

        primality_text = "OK"
        distinct_text = "OK"

    else:

        primality_text = "NO APLICA"
        distinct_text = "NO APLICA"

    if ONLY_PRIMES:
        range_title = "RANGO DE PRIMOS:"
        min_label = "Primo minimo"
        max_label = "Primo maximo"
    else:
        range_title = "RANGO DE VALORES:"
        min_label = "Valor minimo"
        max_label = "Valor maximo"

    if ONLY_PRIMES:
        primality_text = "Primalidad: OK"
    else:
        primality_text = "Primalidad: NO APLICA"

    if ONLY_PRIMES:
        distinct_text = "Primos distintos: OK"
    else:
        distinct_text = "Valores distintos: OK"

    report = [

        "MS_ANALYZER_GENERAL_V2.1",
        "",
        "RESULTADO DEL ANALISIS",
        "",
        f"Archivo: {CSV_FILE}",
        f"Orden de la matriz: {n}x{n}",
        f"Elementos analizados: {total_elements}",
        "",
        "VALIDACIONES:",
        primality_text,
        distinct_text,
        "Magicidad: OK",
        f"Primos consecutivos: {consecutive_text}",
        range_title,
        f"{min_label}: {min_prime}",
        f"{max_label}: {max_prime}",
        f"Rango de {'primos' if ONLY_PRIMES else 'valores'}: {prime_range}",
        "",
        f"Centro C (fila,columna): {center_position}",
        f"Valor central C: {center_value}",
        "",
        f"Suma magica SM: {magic_sum}",
        f"N * C: {n_times_c}",
        f"SM - N * C: {difference}",
        "",
        f"Resultado: {relation}",
        "",
        f"Tiempo total: {total_time:.2f} segundos"

    ]



    write_report(report)



    # Salida compacta en consola

    print()
    print("ANALISIS COMPLETADO CORRECTAMENTE")
    print(f"N = {n}")
    print(f"Elementos = {total_elements}")

    if ONLY_PRIMES:
        print(f"Primo minimo = {min_prime}")
        print(f"Primo maximo = {max_prime}")
    else:
        print(f"Valor minimo = {min_prime}")
        print(f"Valor maximo = {max_prime}")

    print(f"C = {center_value}")
    print(f"SM = {magic_sum}")
    print(f"N * C = {n_times_c}")
    print(f"SM - N * C = {difference}")
    print(
        f"Primos consecutivos = {consecutive_text}"
    )
    print(f"Tiempo total: {total_time:.2f} segundos")
    print()
    print(f"Reporte generado: {REPORT_FILE}")
# =============================================================================
# 6 PROGRAMA PRINCIPAL
# =============================================================================


def main():

    (
        CSV_FILE,
        REPORT_FILE,
        MAGIC_SIZE,
        ONLY_PRIMES
    ) = get_matrix_parameters()

    start_time = time.perf_counter()


    # -------------------------------------------------------------------------
    # Análisis streaming de la matriz
    # -------------------------------------------------------------------------

    result = analyze_streaming(start_time)



    if result is None:

        return



    (
        n,
        magic_sum,
        center_value,
        min_prime,
        max_prime
    ) = result

    # -------------------------------------------------------------------------
    # Verificación de primos consecutivos
    # -------------------------------------------------------------------------

    if ONLY_PRIMES:

        prime_count = count_primes_segmented(
            min_prime,
            max_prime
        )

        consecutive_primes = (
            prime_count == MAGIC_SIZE * MAGIC_SIZE
        )

    else:

        consecutive_primes = False

    # -------------------------------------------------------------------------
    # Reporte final
    # -------------------------------------------------------------------------

    generate_final_report(
        n,
        magic_sum,
        center_value,
        start_time,
        min_prime,
        max_prime,
        consecutive_primes,
        only_primes=ONLY_PRIMES
    )



# =============================================================================
# EJECUCION
# =============================================================================


if __name__ == "__main__":

    main()
                        

                
