"""Join de dos tablas tabulares por una clave comun."""


def join_by_key(
    left: list[dict],
    right: list[dict],
    key: str,
    how: str = "inner",
) -> list[dict]:
    """Une dos listas de dicts por una clave comun.

    Soporta los cuatro tipos de join: inner, left, right, outer.
    Campos duplicados del lado right (distintos a la clave) se sufijan con _right.

    Algoritmo O(n+m): indexa right por key, luego itera left buscando matches.

    Args:
        left: Lista de dicts del lado izquierdo.
        right: Lista de dicts del lado derecho.
        key: Nombre del campo clave para el join.
        how: Tipo de join: inner, left, right, outer.

    Returns:
        Lista de dicts con campos de ambos lados mergeados.
        Campos del right ausentes en un match left se rellenan con None.
        Campos del left ausentes en un match right se rellenan con None.
    """
    # Indexar right por key
    right_index: dict[any, list[dict]] = {}
    for row in right:
        k = row.get(key)
        right_index.setdefault(k, []).append(row)

    # Determinar campos del right que podrian colisionar con left
    left_keys = {k for row in left for k in row}
    right_only_keys = {k for row in right for k in row if k != key}
    conflicting = right_only_keys & left_keys - {key}

    def _merge(l_row: dict | None, r_row: dict | None) -> dict:
        result: dict = {}
        if l_row is not None:
            result.update(l_row)
        if r_row is not None:
            for k, v in r_row.items():
                if k == key:
                    continue
                if k in conflicting:
                    result[f"{k}_right"] = v
                else:
                    result[k] = v
        return result

    def _empty_left(left_sample: dict | None) -> dict:
        if left_sample is None:
            return {}
        return {k: None for k in left_sample}

    def _empty_right() -> dict:
        result: dict = {}
        for row in right:
            for k in row:
                if k == key:
                    continue
                dest = f"{k}_right" if k in conflicting else k
                result[dest] = None
        return result

    matched_right_keys: set = set()
    output: list[dict] = []

    for l_row in left:
        k = l_row.get(key)
        r_rows = right_index.get(k)
        if r_rows:
            matched_right_keys.add(k)
            for r_row in r_rows:
                output.append(_merge(l_row, r_row))
        else:
            if how in ("left", "outer"):
                output.append(_merge(l_row, None) | _empty_right())

    if how in ("right", "outer"):
        for r_row in right:
            k = r_row.get(key)
            if k not in matched_right_keys:
                base = _empty_right()
                base[key] = k
                for rk, rv in r_row.items():
                    if rk == key:
                        continue
                    dest = f"{rk}_right" if rk in conflicting else rk
                    base[dest] = rv
                output.append(base)

    return output
