"""
Archivio documenti - gestionale web per file e cartelle personali.

Ruoli:
  utente          -> vede e modifica solo il proprio spazio
  protocollatore  -> vede e modifica gli spazi di tutto il personale
  admin           -> gestisce gli account (e ha un proprio spazio personale)
"""
import os
import re
import secrets
import shutil
import sqlite3
import time
import unicodedata
from datetime import datetime
from functools import wraps
from pathlib import Path

import click
from flask import (Flask, abort, flash, g, redirect, render_template, request,
                   send_file, session, url_for)
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.security import check_password_hash, generate_password_hash

# --------------------------------------------------------------------------
# Configurazione
# --------------------------------------------------------------------------
DATA_DIR = Path(os.environ.get("GESTIONALE_DATA", "/srv/gestionale/data")).resolve()
SPAZI_DIR = DATA_DIR / "spazi"        # file degli utenti: spazi/u<id>/...
VERSIONI_DIR = DATA_DIR / "versioni"  # copie precedenti dei file modificati
CESTINO_DIR = DATA_DIR / "cestino"    # file e cartelle eliminati
DB_PATH = DATA_DIR / "gestionale.db"

ROLES = {
    "utente": "Utente",
    "protocollatore": "Protocollatore",
    "admin": "Amministratore di sistema",
}
TEXT_EXT = {".txt", ".md", ".csv", ".json", ".xml", ".log", ".ini", ".conf", ".yaml", ".yml"}
INLINE_TYPES = {
    ".pdf": "application/pdf",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".webp": "image/webp",
    ".txt": "text/plain",
}
MAX_EDIT_BYTES = 2 * 1024 * 1024
MIN_PASSWORD = 10
MAX_RISULTATI_RICERCA = 500

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
app.config.update(
    SECRET_KEY=os.environ.get("GESTIONALE_SECRET_KEY") or secrets.token_hex(32),
    MAX_CONTENT_LENGTH=int(os.environ.get("GESTIONALE_MAX_UPLOAD_MB", "200")) * 1024 * 1024,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
    SESSION_COOKIE_SECURE=os.environ.get("GESTIONALE_HTTPS", "0") == "1",
    PERMANENT_SESSION_LIFETIME=8 * 3600,  # sessione valida per una giornata lavorativa
)

SCHEMA = """
CREATE TABLE IF NOT EXISTS utenti (
    id             INTEGER PRIMARY KEY AUTOINCREMENT,
    username       TEXT UNIQUE NOT NULL COLLATE NOCASE,
    nome           TEXT NOT NULL,
    password_hash  TEXT NOT NULL,
    ruolo          TEXT NOT NULL CHECK (ruolo IN ('utente','protocollatore','admin')),
    attivo         INTEGER NOT NULL DEFAULT 1,
    creato_il      TEXT NOT NULL,
    ultimo_accesso TEXT
);
CREATE TABLE IF NOT EXISTS registro (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    quando     TEXT NOT NULL,
    utente_id  INTEGER,
    spazio_id  INTEGER,
    azione     TEXT NOT NULL,
    percorso   TEXT,
    dettaglio  TEXT,
    ip         TEXT
);
CREATE INDEX IF NOT EXISTS idx_registro_spazio ON registro(spazio_id, quando);
CREATE INDEX IF NOT EXISTS idx_registro_quando ON registro(quando);
"""


# --------------------------------------------------------------------------
# Database
# --------------------------------------------------------------------------
def now():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def init_db():
    for d in (SPAZI_DIR, VERSIONI_DIR, CESTINO_DIR):
        d.mkdir(parents=True, exist_ok=True)
    db = sqlite3.connect(DB_PATH)
    db.executescript(SCHEMA)
    db.execute("PRAGMA journal_mode=WAL")
    db.commit()
    db.close()


def get_db():
    if "db" not in g:
        g.db = sqlite3.connect(DB_PATH, timeout=10)
        g.db.row_factory = sqlite3.Row
    return g.db


@app.teardown_appcontext
def close_db(_exc):
    db = g.pop("db", None)
    if db is not None:
        db.close()


def log(azione, spazio_id=None, percorso="", dettaglio=""):
    user = g.get("user")
    db = get_db()
    db.execute(
        "INSERT INTO registro (quando, utente_id, spazio_id, azione, percorso, dettaglio, ip)"
        " VALUES (?,?,?,?,?,?,?)",
        (now(), user["id"] if user else None, spazio_id, azione, percorso, dettaglio,
         request.remote_addr),
    )
    db.commit()


# --------------------------------------------------------------------------
# Percorsi e nomi file (protezione da path traversal)
# --------------------------------------------------------------------------
_CARATTERI_VIETATI = re.compile(r'[\x00-\x1f\x7f/\\:*?"<>|]')


def clean_name(name):
    """Rende sicuro un nome di file/cartella mantenendo lettere accentate e spazi."""
    name = unicodedata.normalize("NFC", name or "")
    name = _CARATTERI_VIETATI.sub("_", name).strip().strip(".").strip()
    if not name or name in (".", ".."):
        return None
    return name[:200]


def spazio_root(uid):
    root = SPAZI_DIR / f"u{uid}"
    root.mkdir(parents=True, exist_ok=True)
    return root.resolve()


def resolve(uid, rel):
    """Restituisce (radice, percorso) garantendo che resti dentro lo spazio dell'utente."""
    root = spazio_root(uid)
    rel = (rel or "").strip("/")
    target = (root / rel).resolve()
    if target != root and root not in target.parents:
        abort(400)
    return root, target


def rel_of(root, path):
    r = path.relative_to(root).as_posix()
    return "" if r == "." else r


def unique_path(path):
    """Se il file esiste già, genera 'nome (2).ext', 'nome (3).ext', ..."""
    if not path.exists():
        return path
    stem, suffix, n = path.stem, path.suffix, 2
    while True:
        candidate = path.with_name(f"{stem} ({n}){suffix}")
        if not candidate.exists():
            return candidate
        n += 1


def stamp():
    return datetime.now().strftime("%Y%m%d-%H%M%S")


def versions_dir(uid, rel):
    return VERSIONI_DIR / f"u{uid}" / rel


def save_version(uid, rel, target):
    d = versions_dir(uid, rel)
    d.mkdir(parents=True, exist_ok=True)
    shutil.copy2(target, d / f"{stamp()}__{target.name}")


def human_size(n):
    for unit in ("B", "KB", "MB", "GB"):
        if n < 1024 or unit == "GB":
            return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}".replace(".", ",")
        n /= 1024


def data_it(value):
    """'2026-10-13 11:43:46' -> '13/10/2026 11:43'"""
    try:
        return datetime.strptime(value, "%Y-%m-%d %H:%M:%S").strftime("%d/%m/%Y %H:%M")
    except (TypeError, ValueError):
        return value or ""


app.jinja_env.filters["dimensione"] = human_size
app.jinja_env.filters["data_it"] = data_it


# --------------------------------------------------------------------------
# Autenticazione, permessi, CSRF, intestazioni di sicurezza
# --------------------------------------------------------------------------
def csrf_token():
    if "_csrf" not in session:
        session["_csrf"] = secrets.token_urlsafe(32)
    return session["_csrf"]


app.jinja_env.globals["csrf_token"] = csrf_token


@app.context_processor
def inject_globals():
    return {"ROLES": ROLES}


@app.before_request
def load_user():
    g.user = None
    uid = session.get("uid")
    if uid:
        user = get_db().execute("SELECT * FROM utenti WHERE id=? AND attivo=1", (uid,)).fetchone()
        if user:
            g.user = user
        else:
            session.clear()


@app.before_request
def check_csrf():
    if request.method == "POST":
        sent = request.form.get("_csrf", "")
        if not sent or not secrets.compare_digest(sent, session.get("_csrf", "")):
            abort(400, "Il modulo è scaduto. Ricarica la pagina e riprova.")


@app.after_request
def security_headers(resp):
    resp.headers["X-Content-Type-Options"] = "nosniff"
    resp.headers["X-Frame-Options"] = "SAMEORIGIN"
    resp.headers["Referrer-Policy"] = "same-origin"
    resp.headers["Content-Security-Policy"] = (
        "default-src 'self'; style-src 'self' https://fonts.googleapis.com; "
        "font-src https://fonts.gstatic.com; img-src 'self' data:; frame-ancestors 'self'"
    )
    return resp


def login_required(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        if not g.user:
            return redirect(url_for("login", next=request.full_path))
        return f(*args, **kwargs)
    return wrapper


def roles_required(*roles):
    def deco(f):
        @wraps(f)
        @login_required
        def wrapper(*args, **kwargs):
            if g.user["ruolo"] not in roles:
                abort(403)
            return f(*args, **kwargs)
        return wrapper
    return deco


def get_spazio(uid):
    """Controllo centrale dei permessi: proprio spazio, oppure protocollatore."""
    if g.user["id"] != uid and g.user["ruolo"] != "protocollatore":
        abort(403)
    owner = get_db().execute("SELECT * FROM utenti WHERE id=?", (uid,)).fetchone()
    if not owner:
        abort(404)
    return owner


def back(uid, rel):
    return redirect(url_for("browse", uid=uid, rel=rel or ""))


# --------------------------------------------------------------------------
# Accesso
# --------------------------------------------------------------------------
@app.route("/accesso", methods=["GET", "POST"])
def login():
    if g.user:
        return redirect(url_for("home"))
    if request.method == "POST":
        username = request.form.get("username", "").strip()
        password = request.form.get("password", "")
        db = get_db()
        user = db.execute("SELECT * FROM utenti WHERE username=?", (username,)).fetchone()
        if user and user["attivo"] and check_password_hash(user["password_hash"], password):
            session.clear()
            session["uid"] = user["id"]
            session.permanent = True
            db.execute("UPDATE utenti SET ultimo_accesso=? WHERE id=?", (now(), user["id"]))
            db.commit()
            g.user = user
            log("accesso")
            nxt = request.args.get("next", "")
            if nxt.startswith("/") and not nxt.startswith("//"):
                return redirect(nxt)
            return redirect(url_for("home"))
        time.sleep(1)  # rallenta i tentativi ripetuti
        log("accesso non riuscito", dettaglio=username[:100])
        flash("Nome utente o password non corretti.", "errore")
    return render_template("login.html")


@app.post("/esci")
@login_required
def logout():
    log("uscita")
    session.clear()
    return redirect(url_for("login"))


@app.route("/password", methods=["GET", "POST"])
@login_required
def cambia_password():
    if request.method == "POST":
        attuale = request.form.get("attuale", "")
        nuova = request.form.get("nuova", "")
        conferma = request.form.get("conferma", "")
        if not check_password_hash(g.user["password_hash"], attuale):
            flash("La password attuale non è corretta.", "errore")
        elif len(nuova) < MIN_PASSWORD:
            flash(f"La nuova password deve avere almeno {MIN_PASSWORD} caratteri.", "errore")
        elif nuova != conferma:
            flash("Le due password nuove non coincidono.", "errore")
        else:
            db = get_db()
            db.execute("UPDATE utenti SET password_hash=? WHERE id=?",
                       (generate_password_hash(nuova), g.user["id"]))
            db.commit()
            log("cambio password")
            flash("Password aggiornata.", "ok")
            return redirect(url_for("home"))
    return render_template("password.html")


@app.route("/")
@login_required
def home():
    if g.user["ruolo"] == "protocollatore":
        return redirect(url_for("spazi"))
    return redirect(url_for("browse", uid=g.user["id"]))


# --------------------------------------------------------------------------
# Navigazione file
# --------------------------------------------------------------------------
def describe(root, p):
    st = p.stat()
    is_dir = p.is_dir()
    ext = p.suffix.lower()
    rel = rel_of(root, p)
    return {
        "name": p.name,
        "rel": rel,
        "parent": rel_of(root, p.parent) or "/",
        "is_dir": is_dir,
        "size": st.st_size,
        "mtime": datetime.fromtimestamp(st.st_mtime).strftime("%d/%m/%Y %H:%M"),
        "editable": (not is_dir) and ext in TEXT_EXT and st.st_size <= MAX_EDIT_BYTES,
        "inline": (not is_dir) and ext in INLINE_TYPES,
    }


@app.route("/documenti/<int:uid>/", defaults={"rel": ""})
@app.route("/documenti/<int:uid>/<path:rel>")
@login_required
def browse(uid, rel):
    owner = get_spazio(uid)
    root, target = resolve(uid, rel)
    if not target.exists():
        abort(404)
    if target.is_file():
        return redirect(url_for("download", uid=uid, rel=rel))

    q = request.args.get("q", "").strip()
    if q:
        needle = q.casefold()
        found = []
        for p in target.rglob("*"):
            if not p.is_symlink() and needle in p.name.casefold():
                found.append(p)
                if len(found) >= MAX_RISULTATI_RICERCA:
                    break
        paths = found
    else:
        paths = [p for p in target.iterdir() if not p.is_symlink()]

    entries = [describe(root, p) for p in paths]
    entries.sort(key=lambda e: (not e["is_dir"], e["name"].casefold()))

    rel = rel_of(root, target)
    crumbs, acc = [], []
    for part in rel.split("/") if rel else []:
        acc.append(part)
        crumbs.append({"name": part, "rel": "/".join(acc)})

    return render_template("files.html", owner=owner, rel=rel, entries=entries,
                           crumbs=crumbs, q=q)


@app.get("/scarica/<int:uid>/<path:rel>")
@login_required
def download(uid, rel):
    get_spazio(uid)
    _root, target = resolve(uid, rel)
    if not target.is_file():
        abort(404)
    ext = target.suffix.lower()
    if request.args.get("inline") and ext in INLINE_TYPES:
        return send_file(target, mimetype=INLINE_TYPES[ext], as_attachment=False,
                         download_name=target.name)
    log("download", uid, rel)
    return send_file(target, as_attachment=True, download_name=target.name)


# --------------------------------------------------------------------------
# Operazioni sui file
# --------------------------------------------------------------------------
@app.post("/documenti/<int:uid>/carica")
@login_required
def upload(uid):
    get_spazio(uid)
    rel = request.form.get("path", "")
    root, target = resolve(uid, rel)
    if not target.is_dir():
        abort(400)
    caricati = 0
    for f in request.files.getlist("files"):
        name = clean_name(f.filename)
        if not name:
            continue
        dest = unique_path(target / name)
        f.save(dest)
        caricati += 1
        log("caricamento", uid, rel_of(root, dest))
    if caricati:
        flash(f"Caricati {caricati} file." if caricati > 1 else "File caricato.", "ok")
    else:
        flash("Nessun file selezionato.", "errore")
    return back(uid, rel)


@app.post("/documenti/<int:uid>/cartella")
@login_required
def mkdir(uid):
    get_spazio(uid)
    rel = request.form.get("path", "")
    root, target = resolve(uid, rel)
    name = clean_name(request.form.get("nome"))
    if not name or not target.is_dir():
        flash("Indica un nome valido per la cartella.", "errore")
        return back(uid, rel)
    new = target / name
    if new.exists():
        flash(f"Esiste già un elemento chiamato «{name}».", "errore")
    else:
        new.mkdir()
        log("nuova cartella", uid, rel_of(root, new))
        flash(f"Cartella «{name}» creata.", "ok")
    return back(uid, rel)


@app.post("/documenti/<int:uid>/rinomina")
@login_required
def rename(uid):
    get_spazio(uid)
    root, target = resolve(uid, request.form.get("path", ""))
    if target == root or not target.exists():
        abort(400)
    parent_rel = rel_of(root, target.parent)
    name = clean_name(request.form.get("nuovo_nome"))
    if not name:
        flash("Indica un nome valido.", "errore")
        return back(uid, parent_rel)
    new = target.parent / name
    if new == target:
        return back(uid, parent_rel)
    if new.exists():
        flash(f"Esiste già un elemento chiamato «{name}».", "errore")
        return back(uid, parent_rel)
    old_rel = rel_of(root, target)
    target.rename(new)
    new_rel = rel_of(root, new)
    old_versions = versions_dir(uid, old_rel)
    if old_versions.exists():  # le versioni seguono il file
        new_versions = versions_dir(uid, new_rel)
        new_versions.parent.mkdir(parents=True, exist_ok=True)
        old_versions.rename(new_versions)
    log("rinomina", uid, new_rel, f"da {old_rel}")
    flash(f"Rinominato in «{name}».", "ok")
    return back(uid, parent_rel)


@app.post("/documenti/<int:uid>/elimina")
@login_required
def delete(uid):
    get_spazio(uid)
    root, target = resolve(uid, request.form.get("path", ""))
    if target == root or not target.exists():
        abort(400)
    rel = rel_of(root, target)
    dest = CESTINO_DIR / f"u{uid}" / stamp() / rel
    dest.parent.mkdir(parents=True, exist_ok=True)
    shutil.move(str(target), str(dest))
    log("eliminazione", uid, rel, "spostato nel cestino")
    flash(f"«{target.name}» spostato nel cestino.", "ok")
    return back(uid, rel_of(root, target.parent))


@app.post("/documenti/<int:uid>/sostituisci")
@login_required
def replace(uid):
    get_spazio(uid)
    root, target = resolve(uid, request.form.get("path", ""))
    f = request.files.get("file")
    if not target.is_file() or not f or not f.filename:
        abort(400)
    rel = rel_of(root, target)
    save_version(uid, rel, target)
    f.save(target)
    log("nuova versione", uid, rel, f"caricato {clean_name(f.filename) or ''}")
    flash(f"«{target.name}» aggiornato. La versione precedente è stata conservata.", "ok")
    return back(uid, rel_of(root, target.parent))


@app.route("/modifica/<int:uid>/<path:rel>", methods=["GET", "POST"])
@login_required
def edit(uid, rel):
    owner = get_spazio(uid)
    root, target = resolve(uid, rel)
    if (not target.is_file() or target.suffix.lower() not in TEXT_EXT
            or target.stat().st_size > MAX_EDIT_BYTES):
        abort(400)
    rel = rel_of(root, target)
    if request.method == "POST":
        content = request.form.get("contenuto", "").replace("\r\n", "\n")
        save_version(uid, rel, target)
        target.write_text(content, encoding="utf-8")
        log("modifica testo", uid, rel)
        flash(f"Modifiche a «{target.name}» salvate.", "ok")
        return back(uid, rel_of(root, target.parent))
    try:
        content = target.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        flash("Questo file non è in formato testo UTF-8 e non può essere modificato qui.", "errore")
        return back(uid, rel_of(root, target.parent))
    return render_template("edit.html", owner=owner, rel=rel, name=target.name,
                           parent=rel_of(root, target.parent), content=content)


# --------------------------------------------------------------------------
# Versioni precedenti
# --------------------------------------------------------------------------
def list_versions(uid, rel):
    d = versions_dir(uid, rel)
    if not d.is_dir():
        return []
    out = []
    for p in sorted((p for p in d.iterdir() if p.is_file()), reverse=True):
        ts = p.name.split("__", 1)[0]
        try:
            quando = datetime.strptime(ts, "%Y%m%d-%H%M%S").strftime("%d/%m/%Y %H:%M:%S")
        except ValueError:
            quando = ts
        out.append({"id": p.name, "quando": quando, "size": p.stat().st_size})
    return out


def version_file(uid, rel, vid):
    if not vid or "/" in vid or "\\" in vid or vid.startswith("."):
        abort(400)
    p = versions_dir(uid, rel) / vid
    if not p.is_file():
        abort(404)
    return p


@app.get("/versioni/<int:uid>/<path:rel>")
@login_required
def versions(uid, rel):
    owner = get_spazio(uid)
    root, target = resolve(uid, rel)
    if not target.is_file():
        abort(404)
    rel = rel_of(root, target)
    return render_template("versioni.html", owner=owner, rel=rel, name=target.name,
                           parent=rel_of(root, target.parent),
                           versioni=list_versions(uid, rel))


@app.get("/versioni/<int:uid>/<path:rel>/scarica")
@login_required
def version_download(uid, rel):
    get_spazio(uid)
    root, target = resolve(uid, rel)
    p = version_file(uid, rel_of(root, target), request.args.get("v"))
    return send_file(p, as_attachment=True, download_name=target.name)


@app.post("/versioni/<int:uid>/<path:rel>/ripristina")
@login_required
def version_restore(uid, rel):
    get_spazio(uid)
    root, target = resolve(uid, rel)
    if not target.is_file():
        abort(404)
    rel = rel_of(root, target)
    p = version_file(uid, rel, request.form.get("v"))
    save_version(uid, rel, target)
    shutil.copy2(p, target)
    log("ripristino versione", uid, rel, p.name)
    flash(f"Ripristinata la versione scelta di «{target.name}».", "ok")
    return redirect(url_for("versions", uid=uid, rel=rel))


# --------------------------------------------------------------------------
# Protocollatori: elenco degli spazi del personale
# --------------------------------------------------------------------------
def space_stats(uid):
    root = SPAZI_DIR / f"u{uid}"
    files, size = 0, 0
    if root.exists():
        for p in root.rglob("*"):
            if p.is_file() and not p.is_symlink():
                files += 1
                size += p.stat().st_size
    return files, size


@app.get("/spazi")
@roles_required("protocollatore")
def spazi():
    q = request.args.get("q", "").strip()
    sql = "SELECT * FROM utenti"
    args = ()
    if q:
        sql += " WHERE nome LIKE ? OR username LIKE ?"
        args = (f"%{q}%", f"%{q}%")
    rows = []
    for u in get_db().execute(sql + " ORDER BY nome COLLATE NOCASE", args).fetchall():
        files, size = space_stats(u["id"])
        rows.append({"u": u, "files": files, "size": size})
    return render_template("spazi.html", rows=rows, q=q)


@app.get("/registro")
@roles_required("protocollatore", "admin")
def registro():
    db = get_db()
    spazio = request.args.get("spazio", type=int)
    sql = """SELECT r.*, a.nome AS autore, s.nome AS spazio_nome
             FROM registro r
             LEFT JOIN utenti a ON a.id = r.utente_id
             LEFT JOIN utenti s ON s.id = r.spazio_id"""
    args = ()
    if spazio:
        sql += " WHERE r.spazio_id = ?"
        args = (spazio,)
    voci = db.execute(sql + " ORDER BY r.id DESC LIMIT 300", args).fetchall()
    utenti = db.execute("SELECT id, nome FROM utenti ORDER BY nome COLLATE NOCASE").fetchall()
    return render_template("registro.html", voci=voci, utenti=utenti, spazio=spazio)


# --------------------------------------------------------------------------
# Amministrazione utenti
# --------------------------------------------------------------------------
@app.get("/amministrazione/utenti")
@roles_required("admin")
def admin_utenti():
    utenti = get_db().execute("SELECT * FROM utenti ORDER BY nome COLLATE NOCASE").fetchall()
    return render_template("utenti.html", utenti=utenti)


@app.route("/amministrazione/utenti/nuovo", methods=["GET", "POST"])
@roles_required("admin")
def admin_nuovo():
    form = request.form
    if request.method == "POST":
        username = form.get("username", "").strip().lower()
        nome = form.get("nome", "").strip()
        ruolo = form.get("ruolo", "utente")
        password = form.get("password", "")
        if not re.fullmatch(r"[a-z0-9._-]{3,40}", username):
            flash("Nome utente: 3-40 caratteri tra lettere minuscole, numeri, punto, trattino.", "errore")
        elif not nome:
            flash("Indica nome e cognome.", "errore")
        elif ruolo not in ROLES:
            flash("Ruolo non valido.", "errore")
        elif len(password) < MIN_PASSWORD:
            flash(f"La password deve avere almeno {MIN_PASSWORD} caratteri.", "errore")
        else:
            db = get_db()
            try:
                cur = db.execute(
                    "INSERT INTO utenti (username, nome, password_hash, ruolo, creato_il)"
                    " VALUES (?,?,?,?,?)",
                    (username, nome, generate_password_hash(password), ruolo, now()))
                db.commit()
            except sqlite3.IntegrityError:
                flash(f"Il nome utente «{username}» è già in uso.", "errore")
            else:
                spazio_root(cur.lastrowid)
                log("nuovo utente", cur.lastrowid, dettaglio=f"{username} ({ruolo})")
                flash(f"Utente {nome} creato.", "ok")
                return redirect(url_for("admin_utenti"))
    return render_template("utente_form.html", u=None, form=form)


@app.route("/amministrazione/utenti/<int:uid>", methods=["GET", "POST"])
@roles_required("admin")
def admin_modifica(uid):
    db = get_db()
    u = db.execute("SELECT * FROM utenti WHERE id=?", (uid,)).fetchone()
    if not u:
        abort(404)
    form = request.form
    if request.method == "POST":
        nome = form.get("nome", "").strip()
        ruolo = form.get("ruolo", u["ruolo"])
        attivo = 1 if form.get("attivo") else 0
        password = form.get("password", "")
        if uid == g.user["id"] and (not attivo or ruolo != "admin"):
            flash("Non puoi disattivare il tuo account né togliere a te stesso il ruolo di amministratore.", "errore")
        elif not nome or ruolo not in ROLES:
            flash("Controlla nome e ruolo.", "errore")
        elif password and len(password) < MIN_PASSWORD:
            flash(f"La nuova password deve avere almeno {MIN_PASSWORD} caratteri.", "errore")
        else:
            db.execute("UPDATE utenti SET nome=?, ruolo=?, attivo=? WHERE id=?",
                       (nome, ruolo, attivo, uid))
            if password:
                db.execute("UPDATE utenti SET password_hash=? WHERE id=?",
                           (generate_password_hash(password), uid))
            db.commit()
            dettagli = [f"ruolo {ruolo}", "attivo" if attivo else "disattivato"]
            if password:
                dettagli.append("password reimpostata")
            log("modifica utente", uid, dettaglio=", ".join(dettagli))
            flash(f"Utente {nome} aggiornato.", "ok")
            return redirect(url_for("admin_utenti"))
    return render_template("utente_form.html", u=u, form=form)


# --------------------------------------------------------------------------
# Errori
# --------------------------------------------------------------------------
ERRORI = {
    400: "Richiesta non valida.",
    403: "Non hai i permessi per accedere a questa risorsa.",
    404: "Il file o la cartella non esiste più, oppure l'indirizzo non è corretto.",
    413: "Il file supera la dimensione massima consentita.",
}


@app.errorhandler(400)
@app.errorhandler(403)
@app.errorhandler(404)
@app.errorhandler(413)
def errore(e):
    desc = e.description if e.code == 400 and e.description and "browser" not in e.description else None
    return render_template("errore.html", code=e.code, messaggio=desc or ERRORI[e.code]), e.code


# --------------------------------------------------------------------------
# Comandi da terminale
# --------------------------------------------------------------------------
@app.cli.command("inizializza")
def cmd_inizializza():
    """Crea cartelle dati e database."""
    init_db()
    click.echo(f"Database pronto in {DB_PATH}")


@app.cli.command("crea-utente")
@click.option("--username", prompt="Nome utente")
@click.option("--nome", prompt="Nome e cognome")
@click.option("--ruolo", prompt="Ruolo", default="admin", type=click.Choice(list(ROLES)))
@click.password_option(prompt="Password")
def cmd_crea_utente(username, nome, ruolo, password):
    """Crea un utente (utile per il primo amministratore)."""
    if len(password) < MIN_PASSWORD:
        raise click.ClickException(f"La password deve avere almeno {MIN_PASSWORD} caratteri.")
    init_db()
    db = sqlite3.connect(DB_PATH)
    try:
        cur = db.execute(
            "INSERT INTO utenti (username, nome, password_hash, ruolo, creato_il) VALUES (?,?,?,?,?)",
            (username.strip().lower(), nome.strip(), generate_password_hash(password), ruolo, now()))
        db.commit()
    except sqlite3.IntegrityError:
        raise click.ClickException("Nome utente già esistente.")
    finally:
        db.close()
    spazio_root(cur.lastrowid)
    click.echo(f"Utente '{username}' creato con ruolo {ruolo}.")


init_db()

if __name__ == "__main__":
    app.run(debug=True)
