#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Herramienta propia de Criptografía Clásica — MCY580 Actividad 2
Autor: Dennis Paul Barrios Ochoa (herramienta desarrollada para la actividad)
Implementa: Vigenère (mod 27/26/191), Hill (digráfico/trigráfico/n-gráfico),
criptoanálisis por Kasiski (Vigenère) y Gauss-Jordan (Hill).
"""
import re
import math
from collections import Counter

# ── Alfabetos ──────────────────────────────────────────────────────────────
ALF_27 = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"          # español, 27
ALF_26 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"           # inglés, 26
# mod 191: imprimibles ASCII extendido (Latin-1): 32..126 + 160..255
ALF_191 = "".join(chr(c) for c in list(range(32, 127)) + list(range(160, 256)))
assert len(ALF_27) == 27 and len(ALF_26) == 26 and len(ALF_191) == 191

ACENTOS = str.maketrans({
    "Á": "A", "É": "E", "Í": "I", "Ó": "O", "Ú": "U", "Ü": "U",
    "á": "a", "é": "e", "í": "i", "ó": "o", "ú": "u", "ü": "u",
    "À": "A", "È": "E", "Ì": "I", "Ò": "O", "Ù": "U",
    "à": "a", "è": "e", "ì": "i", "ò": "o", "ù": "u",
})

def normalizar_es(texto):
    """Mayúsculas, sin acentos, solo A-ZÑ."""
    t = texto.translate(ACENTOS).upper()
    return "".join(c for c in t if c in ALF_27)

def normalizar_en(texto):
    t = texto.translate(ACENTOS).upper()
    return "".join(c for c in t if c in ALF_26)

def bloques(texto, n):
    return " ".join(texto[i:i+n] for i in range(0, len(texto), n))

# ── Vigenère ───────────────────────────────────────────────────────────────
def vigenere(texto, clave, alfabeto, cifrar=True):
    mod = len(alfabeto)
    idx = {c: i for i, c in enumerate(alfabeto)}
    clave_n = [idx[c] for c in clave if c in idx]
    if not clave_n:
        raise ValueError("Clave vacía tras filtrar")
    out = []
    j = 0
    for c in texto:
        if c not in idx:
            out.append(c)          # carácter fuera de alfabeto: se conserva
            continue
        v = idx[c]
        k = clave_n[j % len(clave_n)]
        j += 1
        out.append(alfabeto[(v + k) % mod] if cifrar else alfabeto[(v - k) % mod])
    return "".join(out)

# ── Hill ───────────────────────────────────────────────────────────────────
def egcd(a, b):
    if b == 0:
        return (a, 1, 0)
    g, x, y = egcd(b, a % b)
    return (g, y, x - (a // b) * y)

def inv_mod(a, m):
    g, x, _ = egcd(a % m, m)
    if g != 1:
        raise ValueError(f"{a} no invertible mod {m}")
    return x % m

def mat_inv_mod(mat, m):
    """Inversa de matriz cuadrada mod m por Gauss-Jordan."""
    n = len(mat)
    aug = [row[:] + [1 if i == j else 0 for j in range(n)]
           for i, row in enumerate(mat)]
    for col in range(n):
        piv = next((r for r in range(col, n) if aug[r][col] % m != 0), None)
        if piv is None:
            raise ValueError("Matriz no invertible")
        aug[col], aug[piv] = aug[piv], aug[col]
        inv = inv_mod(aug[col][col], m)
        aug[col] = [(x * inv) % m for x in aug[col]]
        for r in range(n):
            if r != col and aug[r][col] % m != 0:
                f = aug[r][col]
                aug[r] = [(aug[r][k] - f * aug[col][k]) % m for k in range(2 * n)]
    return [row[n:] for row in aug]

def mat_mul_vec(mat, vec, m):
    return [sum(mat[i][j] * vec[j] for j in range(len(vec))) % m
            for i in range(len(mat))]

def hill(texto, clave_mat, alfabeto, cifrar=True, tam=None):
    mod = len(alfabeto)
    idx = {c: i for i, c in enumerate(alfabeto)}
    n = tam or len(clave_mat)
    if cifrar:
        mat = [[x % mod for x in row] for row in clave_mat]
    else:
        mat = mat_inv_mod([[x % mod for x in row] for row in clave_mat], mod)
    # filtrar
    filt = [c for c in texto if c in idx]
    if len(filt) % n:
        # rellenar con el primer carácter del alfabeto (o X)
        pad = alfabeto[0]
        filt += [pad] * (n - len(filt) % n)
    out = []
    for i in range(0, len(filt), n):
        vec = [idx[c] for c in filt[i:i+n]]
        res = mat_mul_vec(mat, vec, mod)
        out.extend(alfabeto[v] for v in res)
    return "".join(out)

# ── Criptoanálisis Vigenère (Kasiski + frecuencia) ─────────────────────────
def kasiski_longitud(texto, max_len=20, min_rep=3):
    """Detecta longitud de clave por repeticiones (Kasiski)."""
    distancias = []
    for n in range(3, 6):
        for i in range(len(texto) - n):
            frag = texto[i:i+n]
            for j in range(i + n, len(texto) - n + 1):
                if texto[j:j+n] == frag:
                    distancias.append(j - i)
    if not distancias:
        return None
    # gcd de todas las distancias
    g = 0
    for d in distancias:
        g = math.gcd(g, d)
    return g if g > 1 else None

FREC_ES = "EAOSNRIDLCTUMPBGVQÑHZJXKFWY"
def mejor_desplazamiento(col, alfabeto):
    """Busca el shift que más acerca la distribución a la del español."""
    mod = len(alfabeto)
    idx = {c: i for i, c in enumerate(alfabeto)}
    freq_target = {c: FREC_ES.index(c) for c in FREC_ES if c in idx}
    n = len(col)
    if n == 0:
        return 0
    best, best_score = 0, -1
    for shift in range(mod):
        # contar frecuencia de cada letra tras des-shiftear
        counts = Counter()
        for c in col:
            counts[alfabeto[(idx[c] - shift) % mod]] += 1
        score = sum(counts.get(c, 0) * (mod - rank)
                    for rank, c in enumerate(FREC_ES))
        if score > best_score:
            best_score, best = score, shift
    return best

def criptoanalizar_vigenere(texto, alfabeto, longitud=None):
    """Devuelve (clave, texto_claro)."""
    idx = {c: i for i, c in enumerate(alfabeto)}
    if longitud is None:
        longitud = kasiski_longitud(texto) or 1
    cols = [[] for _ in range(longitud)]
    for i, c in enumerate(texto):
        if c in idx:
            cols[i % longitud].append(c)
    clave = "".join(alfabeto[mejor_desplazamiento(col, alfabeto)] for col in cols)
    claro = vigenere(texto, clave, alfabeto, cifrar=False)
    return clave, claro

# ── Criptoanálisis Hill (plaintext conocido → Gauss-Jordan) ────────────────
def criptoanalizar_hill(pt, ct, alfabeto, n):
    """Dado plaintext y ciphertext (bloques de n), recupera la matriz clave mod m."""
    mod = len(alfabeto)
    idx = {c: i for i, c in enumerate(alfabeto)}
    # P * K = C  →  K = P^-1 * C (convención: columna)
    # tomamos n bloques para formar P (n×n) y C (n×n)
    P = [[idx[c] for c in pt[i*n:(i+1)*n]] for i in range(n)]
    C = [[idx[c] for c in ct[i*n:(i+1)*n]] for i in range(n)]
    # Resolver K tal que P·K = C (K por columnas): K = P⁻¹·C
    P_inv = mat_inv_mod(P, mod)
    K = [[sum(P_inv[i][k] * C[k][j] for k in range(n)) % mod
          for j in range(n)] for i in range(n)]
    return K

if __name__ == "__main__":
    import json, sys
    res = {}

    # ── Ejercicio 1a: Vigenère mod 27, cifrar ──
    M1 = normalizar_es("El final del verano llegó y tú partirás")
    K1 = normalizar_es("Dúo Dinámico")
    C1 = vigenere(M1, K1, ALF_27, cifrar=True)
    res["1a_M_normalizada"] = M1
    res["1a_K_normalizada"] = K1
    res["1a_criptograma"] = C1
    res["1a_bloques5"] = bloques(C1, 5)

    # ── Ejercicio 1b: Vigenère mod 191, descifrar ──
    K1b = "Monólogo del replicante Roy Batty"
    C1b = ("¥ÝÕXá ×ÙâÒÇ ÚäÅâÜ ÝÇÖÜæ ÓÅàç´ ÎâÖê± ÓßàÌ× Ù|¤ØÌ ÔÅáÙÉ ØÅàØÒ "
           "½ÚÙ®Á æàZ¿Ï Ù_MÒË ½ÕÍ_ß r·ÐÞË ÓáâÖ² çç´m¶ ÕêµÚÙ TÝÓÔÚ ÄÓÞÔÙ "
           "áÔÌÃÄ ÐØÖ´Ï Ü¦ÌÔÃ í±àáT ÏÓºÏÑ ÒÓVÙâ ÐÚp´Ü ×ÓÄÓë °Óàâå ±ÜábÞ áËÞÈÖ "
           "ÏÖÖQÙ ÍÐÅÙç Í¶Ûè° lÖâå» ÚOZÝ× ÓÏÖÉÙ ÝÅÛ×Ý ØÉÎ¡© ÄÖç³Á ×Øå»à Öey")
    P1b = vigenere(C1b, K1b, ALF_191, cifrar=False)
    res["1b_clave"] = K1b
    res["1b_claro"] = P1b

    # ── Ejercicio 2a: Hill mod 26, digráfico, cifrar "Merry Christmas" ──
    M2a = normalizar_en("Merry Christmas")
    K2a = [[18, 9], [15, 23]]
    C2a = hill(M2a, K2a, ALF_26, cifrar=True)
    res["2a_M"] = M2a
    res["2a_K"] = K2a
    res["2a_criptograma"] = C2a
    res["2a_bloques2"] = bloques(C2a, 2)

    # ── Ejercicio 2b: Hill mod 27, trigráfico, descifrar ──
    C2b = "QNQWHORESRAUQXBIBVÑDVGYSÑNZJEBLFJIKJ"
    K2b = [[5, 7, 12], [1, 21, 21], [5, 2, 1]]
    P2b = hill(C2b, K2b, ALF_27, cifrar=False)
    res["2b_C"] = C2b
    res["2b_K"] = K2b
    res["2b_claro"] = P2b

    # ── Ejercicio 3a: criptoanálisis Vigenère mod 27 (Kasiski) ──
    C3a = ("EWZYINEMEJIZMLJÑAAVXAWJWLAUWNLWLHXGAAMXFQUVIWATAMXÑPSMRZMDTZÑPET"
           "MCZSVBWASEIOKNBWWLHM WASUBLQQSGIZMDTJWZKIKQPSWBLSNZTÑIVÑWWAEÑRBCFW"
           "TZILETYCWMNEWFWXOPWEGEVHOBRPWMXHWTTMPIFNXMIKTJLLWLEHMÑTLQUFIFXZWV"
           "ÑWSJÑBAZSM BHIAFJLZMTGPPLBFWKNXMPPHGMAONUBAZSLXOCWENLMFMXJBAWIJMYT"
           "NEDHXMIILKÑAXKIUWJDXKQMWMNWAEXFLZATMJWZÑUEMLXOEAMIGQUUTEWSNSJQJDB"
           "VWGSÑGOCIIGEMFWINCWRIHILAÑXPWMXAAULBPWFIAWKBTJITTLYIVIFWAVXWLAIT"
           "ZEUSMRDCTBXÑIFJIOMAW IASEXLEBHBGAAMBFWJDXWAAWLXHMBXFMSSLFWAXBXHGUI"
           "GYPLIWAJAIZÑIXBTZMJÑXAUÑTGAKWLMAXNXWWSSEBPMKTNQZSWXPWVTMHILXJLKSM"
           "RZMMIWLADIMMIAMXO")
    C3a = C3a.replace(" ", "")
    long3a = kasiski_longitud(C3a)
    clave3a, P3a = criptoanalizar_vigenere(C3a, ALF_27, longitud=long3a)
    res["3a_longitud_kasiski"] = long3a
    res["3a_clave"] = clave3a
    res["3a_claro"] = P3a
    res["3a_primeras3"] = " ".join(P3a.split()[:3])

    # ── Ejercicio 3b: criptoanálisis Hill mod 191, bloques de 4 ──
    pt_known = "Dime de qué presumes"
    C3b = "«BÝ. 0Çß$ nãÏ¢ òzd¦ 0È¯R ìàqÕ 0Çß$ UËË´ àTµ8"
    # quitar espacios para trabajar en bloques
    C3b_n = C3b.replace(" ", "")
    # plaintext conocido: solo los primeros 16 chars (4 bloques) para la matriz
    # "Dime de qué pres" = 16 chars exactos
    pt16 = "Dime de qué pres"
    K3b = criptoanalizar_hill(pt16, C3b_n[:16], ALF_191, 4)
    # descifrar todo con la matriz recuperada
    P3b = hill(C3b_n, K3b, ALF_191, cifrar=False, tam=4)
    res["3b_K_recuperada"] = K3b
    res["3b_claro"] = P3b

    print(json.dumps(res, ensure_ascii=False, indent=1))
    with open("/root/vmlab/../malware_lab/../.openclaw/workspace/resultados_act2.json", "w",
              encoding="utf-8") as f:
        json.dump(res, f, ensure_ascii=False, indent=1)
