"""Genera un avatar circular de iniciales tipo foto de perfil.

Dibuja un circulo relleno de color con 1-2 iniciales blancas centradas sobre
fondo transparente y lo exporta como PNG cuadrado. El color de fondo se puede
fijar explicitamente o derivar de forma DETERMINISTA del texto (mismo texto ->
mismo color siempre), lo que produce avatares reconocibles y distintos por
nombre sin necesidad de una imagen real.

Funciones publicas reutilizables:
    derive_initials  — extrae 1-2 iniciales en mayusculas de un nombre
    derive_bg_color  — mapea un texto a un color de paleta de forma estable
"""
import hashlib
import os
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

# Fuente bold preinstalada en este Linux. Si no existe, se cae al default de PIL.
DEFAULT_FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"

# Paleta agradable tipo Tailwind 500-600 (12 colores). El indice se elige de
# forma determinista a partir del hash del texto -> mismo texto, mismo color.
PALETTE = [
    "#0ea5e9",  # sky-500
    "#10b981",  # emerald-500
    "#8b5cf6",  # violet-500
    "#f59e0b",  # amber-500
    "#f43f5e",  # rose-500
    "#6366f1",  # indigo-500
    "#14b8a6",  # teal-500
    "#f97316",  # orange-500
    "#d946ef",  # fuchsia-500
    "#84cc16",  # lime-500
    "#06b6d4",  # cyan-500
    "#ef4444",  # red-500
]

# Factor de supersampling para antialiasing: se renderiza a NxN veces el tamano
# final y se reduce con LANCZOS para obtener bordes suaves.
_SUPERSAMPLE = 4

# Margen del circulo respecto al canvas (~4% por lado).
_MARGIN_RATIO = 0.04

# Tamano de fuente como fraccion del lado del canvas.
_FONT_RATIO = 0.46


def _hex_to_rgb(h: str) -> tuple[int, int, int]:
    """Convierte un color "#RRGGBB" (o "RRGGBB") a una tupla RGB."""
    h = h.lstrip("#")
    if len(h) != 6:
        raise ValueError(f"color hex invalido, se espera #RRGGBB: {h!r}")
    return tuple(int(h[i: i + 2], 16) for i in (0, 2, 4))


def derive_initials(text: str) -> str:
    """Extrae 1-2 iniciales en mayusculas a partir de un nombre.

    Trocea el texto por espacios, guiones y guiones bajos. Toma la primera
    letra alfabetica de los dos primeros tokens que empiecen por letra. Si solo
    un token tiene letra inicial, devuelve 1 inicial. Si ninguno empieza por
    letra, usa el primer caracter alfanumerico del texto completo.

    Args:
        text: Nombre del que derivar las iniciales (ej. "John Doe", "osint_01").

    Returns:
        1 o 2 caracteres en mayusculas. Cadena vacia si no hay nada alfanumerico.
    """
    # Normaliza separadores a espacios.
    normalized = text.replace("-", " ").replace("_", " ")
    tokens = [t for t in normalized.split() if t]

    initials = []
    for token in tokens:
        # Primera letra alfabetica del token (el token debe empezar por letra).
        if token[0].isalpha():
            initials.append(token[0].upper())
        if len(initials) == 2:
            break

    if initials:
        return "".join(initials)

    # Fallback: primer caracter alfanumerico del texto entero.
    for ch in text:
        if ch.isalnum():
            return ch.upper()
    return ""


def derive_bg_color(text: str) -> str:
    """Mapea un texto a un color de la paleta de forma estable y determinista.

    Usa md5 del texto para indexar la paleta, de modo que el mismo texto
    produce siempre el mismo color entre ejecuciones y procesos.

    Args:
        text: Texto del que derivar el color.

    Returns:
        Color en formato "#RRGGBB" de la paleta interna.
    """
    digest = hashlib.md5(text.encode("utf-8")).hexdigest()
    idx = int(digest, 16) % len(PALETTE)
    return PALETTE[idx]


def _load_font(font_size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    """Carga la fuente DejaVu Bold al tamano dado, con fallback al default."""
    try:
        return ImageFont.truetype(DEFAULT_FONT_PATH, font_size)
    except OSError:
        return ImageFont.load_default()


def generate_initials_avatar(
    text: str,
    out_path: str,
    bg_hex: str = "",
    size: int = 256,
    fg_hex: str = "#FFFFFF",
) -> str:
    """Genera un avatar circular de iniciales y lo guarda como PNG.

    Dibuja un circulo relleno con `bg_hex` (o un color derivado de `text` si va
    vacio) y centra 1-2 iniciales en `fg_hex` sobre el. El fondo fuera del
    circulo queda transparente. Renderiza a 4x y reduce con LANCZOS para bordes
    suaves.

    Args:
        text: Nombre del que derivar las iniciales (ej. "Aurgi", "John Doe").
        out_path: Ruta de salida del PNG. El directorio padre se crea si falta.
        bg_hex: Color de fondo del circulo en "#RRGGBB". Si va vacio (""), se
            deriva de forma determinista de `text`.
        size: Lado del PNG cuadrado en pixels (default 256).
        fg_hex: Color del texto en "#RRGGBB" (default blanco "#FFFFFF").

    Returns:
        La misma `out_path` recibida.

    Raises:
        ValueError: Si algun color no tiene formato "#RRGGBB" o `size` <= 0.
        OSError: Si falla la escritura del archivo a disco.
    """
    if size <= 0:
        raise ValueError(f"size debe ser positivo, recibido: {size!r}")

    background = bg_hex if bg_hex else derive_bg_color(text)
    bg_rgb = _hex_to_rgb(background)
    fg_rgb = _hex_to_rgb(fg_hex)

    initials = derive_initials(text)

    # Render a 4x para antialiasing, luego se reduce con LANCZOS.
    big = size * _SUPERSAMPLE
    canvas = Image.new("RGBA", (big, big), (0, 0, 0, 0))
    draw = ImageDraw.Draw(canvas)

    margin = int(big * _MARGIN_RATIO)
    circle_box = [margin, margin, big - margin - 1, big - margin - 1]
    draw.ellipse(circle_box, fill=bg_rgb + (255,))

    if initials:
        font_size = max(1, int(big * _FONT_RATIO))
        font = _load_font(font_size)

        # Bounding box real del glyph (no solo ascent) para centrado optico.
        bbox = draw.textbbox((0, 0), initials, font=font)
        text_w = bbox[2] - bbox[0]
        text_h = bbox[3] - bbox[1]
        # El origen del texto se desplaza por el offset del bbox para que el
        # glyph quede centrado tanto horizontal como verticalmente.
        text_x = (big - text_w) / 2 - bbox[0]
        text_y = (big - text_h) / 2 - bbox[1]
        draw.text((text_x, text_y), initials, font=font, fill=fg_rgb + (255,))

    final = canvas.resize((size, size), Image.LANCZOS)

    out = Path(out_path)
    if not out.is_absolute():
        out = Path.cwd() / out
    out.parent.mkdir(parents=True, exist_ok=True)

    try:
        final.save(out, format="PNG")
    except OSError as exc:
        raise OSError(f"no se pudo escribir el avatar en {out}: {exc}") from exc

    return out_path
