import csv
import os
import re
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from typing import Dict, Iterable, List, Optional, Protocol

import requests

ISTAT_CSV_URL = "https://www.istat.it/storage/codici-unita-amministrative/Elenco-comuni-italiani.csv"

# Percorso della directory "database" basato sul file corrente
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_CSV_PATH = os.path.join(BASE_DIR, "database/comuni.csv")
DEFAULT_TIMEOUT = 10

COLUMN_MAPPING = {
    "denominazione_in_italiano": "denominazione_italiana",
    "sigla_automobilistica": "sigla_provincia",
    "denominazione_regione": "regione",
    "denominazione_dell'unità_territoriale_sovracomunale_(valida_a_fini_statistici)": "provincia",
    "codice_catastale_del_comune": "codice_catastale",
}


class _SessionLike(Protocol):
    def head(self, url: str, **kwargs):
        ...

    def get(self, url: str, **kwargs):
        ...


def clean_column_names(columns: Iterable[str]) -> List[str]:
    """Pulisce i nomi delle colonne per renderli coerenti."""
    cleaned = []
    for col in columns:
        if col is None:
            continue
        col = col.strip()
        col = col.replace("\n", " ")
        col = re.sub(r"\s+", "_", col)
        cleaned.append(col.lower())
    return cleaned


def _parse_http_date(value: str) -> Optional[datetime]:
    """Converte una stringa HTTP-date in datetime."""
    try:
        return parsedate_to_datetime(value)
    except Exception:
        return None


def get_remote_last_modified(session: Optional[_SessionLike] = None, timeout: int = DEFAULT_TIMEOUT) -> Optional[datetime]:
    """Recupera la data dell'ultima modifica del CSV remoto."""
    sess = session or requests
    try:
        response = sess.head(ISTAT_CSV_URL, allow_redirects=True, timeout=timeout)
        response.raise_for_status()
    except Exception as exc:
        print(f"Impossibile ottenere la data di ultima modifica del file remoto: {exc}")
        return None

    remote_last_modified = response.headers.get("Last-Modified")
    if not remote_last_modified:
        return None

    parsed = _parse_http_date(remote_last_modified)
    if parsed and parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed


def get_local_last_modified(local_path: str) -> Optional[datetime]:
    """Recupera la data di ultima modifica del file locale."""
    if os.path.exists(local_path):
        return datetime.fromtimestamp(os.path.getmtime(local_path), tz=timezone.utc)
    return None


def is_csv_updated(local_path: str = DEFAULT_CSV_PATH, session: Optional[requests.Session] = None, timeout: int = DEFAULT_TIMEOUT) -> bool:
    """Controlla se il file CSV locale è aggiornato rispetto alla versione remota."""
    remote_last_modified = get_remote_last_modified(session=session, timeout=timeout)
    local_last_modified = get_local_last_modified(local_path)

    # Se non è possibile ottenere la data remota, consideriamo il file locale valido
    if remote_last_modified is None:
        print("Non è stato possibile verificare l'aggiornamento remoto. Uso il file locale.")
        return os.path.exists(local_path)

    if local_last_modified is None or local_last_modified < remote_last_modified:
        return False  # Non aggiornato
    return True  # Aggiornato


def download_csv(local_path: str = DEFAULT_CSV_PATH, session: Optional[_SessionLike] = None, timeout: int = DEFAULT_TIMEOUT) -> None:
    """Scarica il file CSV dell'ISTAT e lo salva localmente."""
    sess = session or requests
    try:
        response = sess.get(ISTAT_CSV_URL, stream=True, timeout=timeout)
        response.raise_for_status()
    except Exception as exc:
        raise Exception(f"Errore durante il download del file CSV: {exc}") from exc

    os.makedirs(os.path.dirname(local_path), exist_ok=True)
    with open(local_path, "wb") as file:
        for chunk in response.iter_content(chunk_size=8192):
            file.write(chunk)
    print(f"CSV scaricato correttamente: {local_path}")


def _ensure_local_csv(csv_file: str, allow_download: bool, session: Optional[requests.Session], timeout: int) -> None:
    """Verifica che il CSV esista e sia aggiornato, eventualmente scaricandolo."""
    if not allow_download:
        if not os.path.exists(csv_file):
            raise FileNotFoundError(f"File CSV non trovato e download disattivato: {csv_file}")
        return

    if not os.path.exists(csv_file):
        print("Il file CSV non esiste. Download in corso...")
        download_csv(csv_file, session=session, timeout=timeout)
        return

    if not is_csv_updated(csv_file, session=session, timeout=timeout):
        print("Il file CSV non è aggiornato. Download in corso...")
        download_csv(csv_file, session=session, timeout=timeout)
    else:
        print("Il file CSV è già aggiornato.")


def _read_raw_rows(csv_file: str) -> List[Dict[str, str]]:
    """Legge il CSV grezzo e restituisce righe con intestazioni pulite."""

    def _read_with_encoding(encoding: str) -> List[Dict[str, str]]:
        with open(csv_file, "r", encoding=encoding, newline="") as handle:
            reader = csv.reader(handle, delimiter=";")
            try:
                header = next(reader)
            except StopIteration:
                raise ValueError("Il file CSV è vuoto.")

            cleaned_header = clean_column_names(header)
            rows: List[Dict[str, str]] = []
            for raw_row in reader:
                if not raw_row or not any(raw_row):
                    continue
                row_data = {}
                for idx, value in enumerate(raw_row):
                    if idx >= len(cleaned_header):
                        continue
                    row_data[cleaned_header[idx]] = value.strip()
                rows.append(row_data)
            return rows

    try:
        return _read_with_encoding("utf-8")
    except UnicodeDecodeError:
        return _read_with_encoding("latin1")


def _extract_relevant_columns(rows: List[Dict[str, str]]) -> List[Dict[str, str]]:
    """Riduce le righe alle colonne di interesse rinominate."""
    cleaned_rows: List[Dict[str, str]] = []
    for row in rows:
        record: Dict[str, str] = {}
        for raw_column, target_column in COLUMN_MAPPING.items():
            record[target_column] = row.get(raw_column, "")

        if not record.get("denominazione_italiana"):
            continue  # ignora righe incomplete
        cleaned_rows.append(record)
    return cleaned_rows


def load_comuni_data(csv_file: str = DEFAULT_CSV_PATH, *, allow_download: bool = True, session: Optional[requests.Session] = None, timeout: int = DEFAULT_TIMEOUT) -> List[Dict[str, str]]:
    """
    Carica i dati dei comuni dal file CSV, aggiornandolo se necessario.

    :param csv_file: Percorso del CSV locale.
    :param allow_download: Se True prova a scaricare/aggiornare i dati dal sito ISTAT.
    :param session: Sessione requests riutilizzabile (utile nei test).
    :param timeout: Timeout per le richieste HTTP.
    """
    _ensure_local_csv(csv_file, allow_download=allow_download, session=session, timeout=timeout)
    rows = _read_raw_rows(csv_file)
    return _extract_relevant_columns(rows)
