from datetime import datetime
import unicodedata

MONTH_CODES = {
    1: "A",
    2: "B",
    3: "C",
    4: "D",
    5: "E",
    6: "H",
    7: "L",
    8: "M",
    9: "P",
    10: "R",
    11: "S",
    12: "T",
}

# Mapping locale minimale (scelta esplicita in implementation)
CATASTAL_CODES = {
    "MILANO": "F205",
    "ROMA": "H501",
    "TORINO": "L219",
}

ODD_VALUES = {
    **{str(i): v for i, v in zip(range(10), [1, 0, 5, 7, 9, 13, 15, 17, 19, 21])},
    **dict(zip("ABCDEFGHIJKLMNOPQRSTUVWXYZ", [1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23]))
}

EVEN_VALUES = {
    **{str(i): i for i in range(10)},
    **{c: i for i, c in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ")}
}


def _normalize(text: str) -> str:
    text = unicodedata.normalize("NFD", text)
    text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn")
    return "".join(ch for ch in text.upper() if ch.isalpha())


def _extract_code(value: str, is_name: bool = False) -> str:
    value = _normalize(value)
    consonants = "".join(ch for ch in value if ch not in "AEIOU")
    vowels = "".join(ch for ch in value if ch in "AEIOU")

    if is_name and len(consonants) >= 4:
        return consonants[0] + consonants[2] + consonants[3]

    code = (consonants + vowels + "XXX")[:3]
    return code


def _control_char(partial_cf: str) -> str:
    total = 0
    for index, ch in enumerate(partial_cf, start=1):
        total += ODD_VALUES[ch] if index % 2 == 1 else EVEN_VALUES[ch]
    return chr((total % 26) + ord("A"))


def calcola_codice_fiscale(nome: str, cognome: str, data_nascita: str, luogo: str, sesso: str) -> str:
    surname_code = _extract_code(cognome)
    name_code = _extract_code(nome, is_name=True)

    birth_date = datetime.strptime(data_nascita, "%Y-%m-%d")
    year_code = f"{birth_date.year % 100:02d}"
    month_code = MONTH_CODES[birth_date.month]

    sex = sesso.strip().upper()
    if sex not in {"M", "F"}:
        raise ValueError("Sesso non valido: usare 'M' o 'F'")

    day = birth_date.day + (40 if sex == "F" else 0)
    day_code = f"{day:02d}"

    place_key = _normalize(luogo)
    if place_key not in CATASTAL_CODES:
        raise ValueError(f"Luogo non supportato nel mapping locale: {luogo}")
    place_code = CATASTAL_CODES[place_key]

    partial_cf = f"{surname_code}{name_code}{year_code}{month_code}{day_code}{place_code}"
    return partial_cf + _control_char(partial_cf)


if __name__ == "__main__":
    print(calcola_codice_fiscale("Mario", "Rossi", "1985-06-15", "Milano", "M"))
