"""Auth API routes."""

from __future__ import annotations

import secrets
import string
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field

from pydantic import model_validator

from app import auth, backup_codes, console_handoff, mail_notify
from app.permissions import ROLES, can_manage_users

router = APIRouter(prefix="/api/v1/auth", tags=["auth"])


class LoginRequest(BaseModel):
    username: str
    password: str


class LoginMfaRequest(BaseModel):
    mfa_token: str
    totp_code: str | None = Field(default=None, min_length=6, max_length=6)
    backup_code: str | None = Field(default=None, min_length=8, max_length=12)

    @model_validator(mode="after")
    def require_one_factor(self):
        has_totp = bool(self.totp_code and self.totp_code.strip())
        has_backup = bool(self.backup_code and self.backup_code.strip())
        if has_totp == has_backup:
            raise ValueError("informe o código 2FA ou um código de backup")
        return self


class UserUpdateRequest(BaseModel):
    role: str | None = None
    active: bool | None = None
    password: str | None = Field(default=None, min_length=6)
    display_name: str | None = None


class UserCreateRequest(BaseModel):
    email: str = Field(min_length=3, max_length=320)
    password: str = Field(min_length=6)
    role: str
    display_name: str | None = None
    phone: str | None = Field(default=None, max_length=32)
    active: bool = True


class UserCloneRequest(BaseModel):
    email: str = Field(min_length=3, max_length=320)
    password: str | None = Field(default=None, min_length=6)
    display_name: str | None = None
    active: bool = True


class ChangePasswordRequest(BaseModel):
    current_password: str = Field(min_length=1)
    new_password: str = Field(min_length=8)
    totp_code: str | None = Field(default=None, min_length=6, max_length=6)


@router.post("/login")
def login(body: LoginRequest, request: Request):
    client_ip = request.client.host if request.client else "unknown"
    auth.check_login_rate_limit(client_ip)
    user, row = auth.check_credentials(body.username, body.password)
    if not user:
        raise HTTPException(401, "invalid credentials")
    if auth.user_requires_totp(row):
        mfa_token = auth.create_mfa_token(user.username)
        return {
            "mfa_required": True,
            "mfa_token": mfa_token,
            "expires_in": auth.MFA_TOKEN_TTL_SEC,
            "username": user.username,
        }
    auth.touch_last_login(user.username)
    token, expires_in = auth.create_access_token(user)
    return {
        "access_token": token,
        "token_type": "bearer",
        "expires_in": expires_in,
        "username": user.username,
        "role": user.role,
        "display_name": user.display_name,
    }


@router.post("/login/mfa")
def login_mfa(body: LoginMfaRequest, request: Request):
    client_ip = request.client.host if request.client else "unknown"
    auth.check_login_rate_limit(client_ip)
    username = auth.consume_mfa_token(body.mfa_token)
    if not username:
        raise HTTPException(401, "invalid or expired mfa session")
    if body.backup_code:
        with auth.db() as conn:
            ok = backup_codes.consume_backup_code(conn, username, body.backup_code.strip())
            conn.commit()
        if not ok:
            raise HTTPException(401, "código de backup inválido ou já utilizado")
    elif not auth.verify_user_totp(username, body.totp_code or ""):
        raise HTTPException(401, "invalid authenticator code")
    row = auth._user_row(username)
    if not row or not row["active"]:
        raise HTTPException(401, "user inactive")
    user = auth.DeskUser(
        username=row["username"],
        role=row["role"],
        display_name=row["display_name"],
        active=True,
    )
    auth.touch_last_login(user.username)
    token, expires_in = auth.create_access_token(user)
    return {
        "access_token": token,
        "token_type": "bearer",
        "expires_in": expires_in,
        "username": user.username,
        "role": user.role,
        "display_name": user.display_name,
    }


@router.post("/logout")
def logout(user: auth.DeskUser = Depends(auth.get_current_user)):
    return {"ok": True, "username": user.username}


@router.get("/me")
def me(user: auth.DeskUser = Depends(auth.get_current_user)):
    with auth.db() as conn:
        row = conn.execute(
            """
            SELECT username, role, display_name, active, last_login_at, created_at, updated_at,
                   email, phone, mfa_enabled, totp_enabled
            FROM desk_users WHERE username = ?
            """,
            (user.username,),
        ).fetchone()
    if not row:
        raise HTTPException(404, "user not found")
    out = auth.user_public_dict(row)
    with auth.db() as conn:
        out["backup_codes_remaining"] = backup_codes.count_remaining(conn, user.username)
    return out


@router.post("/change-password")
def change_password(
    body: ChangePasswordRequest,
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    row = auth._user_row(user.username)
    if not row or not row["active"]:
        raise HTTPException(401, "user inactive or not found")
    if not auth.verify_password(body.current_password, row["password_hash"]):
        raise HTTPException(401, "senha atual incorreta")
    if body.current_password == body.new_password:
        raise HTTPException(400, "a nova senha deve ser diferente da atual")
    if auth.user_requires_totp(row):
        code = (body.totp_code or "").strip()
        if not code:
            raise HTTPException(400, "código 2FA obrigatório")
        if not auth.verify_user_totp(user.username, code):
            raise HTTPException(401, "código 2FA inválido")
    now = datetime.now(timezone.utc).isoformat()
    with auth.db() as conn:
        conn.execute(
            "UPDATE desk_users SET password_hash = ?, updated_at = ? WHERE username = ?",
            (auth.hash_password(body.new_password), now, user.username),
        )
        conn.commit()
    return {"ok": True, "message": "Senha alterada com sucesso"}


@router.get("/users")
def list_users(user: auth.DeskUser = Depends(auth.get_current_user)):
    if not can_manage_users(user.role):
        raise HTTPException(403, "insufficient permissions")
    with auth.db() as conn:
        rows = conn.execute(
            """
            SELECT u.username, u.role, u.display_name, u.active, u.last_login_at,
                   u.created_at, u.updated_at, u.email, u.phone, u.mfa_enabled, u.totp_enabled,
                   (SELECT COUNT(*) FROM desk_backup_codes b
                    WHERE b.username = u.username AND b.used_at IS NULL) AS backup_codes_remaining
            FROM desk_users u ORDER BY u.username
            """
        ).fetchall()
    users = []
    for row in rows:
        item = auth.user_public_dict(row)
        item["totp_enabled"] = bool(row["totp_enabled"])
        item["backup_codes_remaining"] = int(row["backup_codes_remaining"] or 0)
        users.append(item)
    return {"users": users}


def _normalize_username(raw: str) -> str:
    target = raw.strip()
    if target.lower() != "root":
        target = target.lower()
    return target


def _fetch_user_public(conn, username: str) -> dict:
    row = conn.execute(
        """
        SELECT u.username, u.role, u.display_name, u.active, u.last_login_at,
               u.created_at, u.updated_at, u.email, u.phone, u.mfa_enabled, u.totp_enabled,
               (SELECT COUNT(*) FROM desk_backup_codes b
                WHERE b.username = u.username AND b.used_at IS NULL) AS backup_codes_remaining
        FROM desk_users u WHERE u.username = ?
        """,
        (username,),
    ).fetchone()
    if not row:
        raise HTTPException(404, "user not found")
    item = auth.user_public_dict(row)
    item["totp_enabled"] = bool(row["totp_enabled"])
    item["backup_codes_remaining"] = int(row["backup_codes_remaining"] or 0)
    return item


@router.post("/users")
def create_user(
    body: UserCreateRequest,
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_manage_users(user.role):
        raise HTTPException(403, "insufficient permissions")
    email = body.email.strip().lower()
    if "@" not in email:
        raise HTTPException(400, "email inválido")
    if body.role not in ROLES:
        raise HTTPException(400, "invalid role")
    if body.role == "super_admin" and user.role != "super_admin":
        raise HTTPException(403, "apenas Super Admin pode criar Super Admin")
    display = (body.display_name or email.split("@")[0]).strip() or email.split("@")[0]
    now = datetime.now(timezone.utc).isoformat()
    with auth.db() as conn:
        exists = conn.execute(
            "SELECT 1 FROM desk_users WHERE username = ? OR email = ?",
            (email, email),
        ).fetchone()
        if exists:
            raise HTTPException(409, "utilizador já existe")
        conn.execute(
            """
            INSERT INTO desk_users
            (username, password_hash, role, display_name, email, phone,
             mfa_enabled, totp_secret, totp_enabled, active, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, 0, NULL, 0, ?, ?, ?)
            """,
            (
                email,
                auth.hash_password(body.password),
                body.role,
                display,
                email,
                body.phone,
                1 if body.active else 0,
                now,
                now,
            ),
        )
        conn.commit()
        item = _fetch_user_public(conn, email)
    return {"user": item, "message": "Utilizador criado"}


@router.patch("/users/{username}")
def update_user(
    username: str,
    body: UserUpdateRequest,
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_manage_users(user.role):
        raise HTTPException(403, "insufficient permissions")
    target = _normalize_username(username)
    updates: list[str] = []
    params: list[object] = []
    if body.role is not None:
        if body.role not in ROLES:
            raise HTTPException(400, "invalid role")
        updates.append("role = ?")
        params.append(body.role)
    if body.active is not None:
        updates.append("active = ?")
        params.append(1 if body.active else 0)
    if body.display_name is not None:
        updates.append("display_name = ?")
        params.append(body.display_name)
    if body.password:
        updates.append("password_hash = ?")
        params.append(auth.hash_password(body.password))
    if not updates:
        raise HTTPException(400, "no fields to update")
    now = datetime.now(timezone.utc).isoformat()
    updates.append("updated_at = ?")
    params.append(now)
    params.append(target)
    with auth.db() as conn:
        cur = conn.execute(
            f"UPDATE desk_users SET {', '.join(updates)} WHERE username = ?",
            params,
        )
        conn.commit()
        if cur.rowcount == 0:
            raise HTTPException(404, "user not found")
        item = _fetch_user_public(conn, target)
    return {"user": item}


@router.delete("/users/{username}")
def delete_user(
    username: str,
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_manage_users(user.role):
        raise HTTPException(403, "insufficient permissions")
    target = _normalize_username(username)
    if target == "root":
        raise HTTPException(400, "não é possível eliminar root")
    if target == user.username.lower():
        raise HTTPException(400, "não pode eliminar a sua própria conta")
    with auth.db() as conn:
        row = conn.execute(
            "SELECT username FROM desk_users WHERE username = ?",
            (target,),
        ).fetchone()
        if not row:
            raise HTTPException(404, "user not found")
        conn.execute("DELETE FROM desk_backup_codes WHERE username = ?", (target,))
        conn.execute("DELETE FROM desk_users WHERE username = ?", (target,))
        conn.commit()
    return {"ok": True, "message": f"Utilizador {target} eliminado"}


@router.post("/users/{username}/clone")
def clone_user(
    username: str,
    body: UserCloneRequest,
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_manage_users(user.role):
        raise HTTPException(403, "insufficient permissions")
    source = _normalize_username(username)
    email = body.email.strip().lower()
    if "@" not in email:
        raise HTTPException(400, "email inválido")
    pwd = body.password or "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12))
    now = datetime.now(timezone.utc).isoformat()
    with auth.db() as conn:
        src = conn.execute(
            "SELECT username, role, display_name FROM desk_users WHERE username = ?",
            (source,),
        ).fetchone()
        if not src:
            raise HTTPException(404, "utilizador origem não encontrado")
        exists = conn.execute(
            "SELECT 1 FROM desk_users WHERE username = ? OR email = ?",
            (email, email),
        ).fetchone()
        if exists:
            raise HTTPException(409, "utilizador destino já existe")
        display = (body.display_name or f"{src['display_name'] or src['username']} (cópia)").strip()
        conn.execute(
            """
            INSERT INTO desk_users
            (username, password_hash, role, display_name, email, phone,
             mfa_enabled, totp_secret, totp_enabled, active, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, NULL, 0, NULL, 0, ?, ?, ?)
            """,
            (
                email,
                auth.hash_password(pwd),
                src["role"],
                display,
                email,
                1 if body.active else 0,
                now,
                now,
            ),
        )
        conn.commit()
        item = _fetch_user_public(conn, email)
    out = {"user": item, "message": "Utilizador copiado"}
    if not body.password:
        out["generated_password"] = pwd
    return out


@router.post("/users/{username}/reset-2fa")
def reset_user_2fa(
    username: str,
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_manage_users(user.role):
        raise HTTPException(403, "insufficient permissions")
    target = _normalize_username(username)
    if target == "root":
        raise HTTPException(400, "não é possível resetar 2FA do root por aqui")
    now = datetime.now(timezone.utc).isoformat()
    with auth.db() as conn:
        row = conn.execute(
            "SELECT username, email, totp_enabled FROM desk_users WHERE username = ?",
            (target,),
        ).fetchone()
        if not row:
            raise HTTPException(404, "user not found")
        if not row["totp_enabled"]:
            raise HTTPException(400, "utilizador não tem 2FA ativo")
        conn.execute(
            """
            UPDATE desk_users
            SET totp_secret = NULL, totp_enabled = 0, mfa_enabled = 0, updated_at = ?
            WHERE username = ?
            """,
            (now, target),
        )
        conn.execute("DELETE FROM desk_backup_codes WHERE username = ?", (target,))
        conn.commit()
    email = row["email"] or target
    mail_notify.notify_admin_2fa_reset(target, email, user.username)
    return {"ok": True, "message": f"2FA resetado para {target}. O utilizador pode reconfigurar no login."}


class ConsoleHandoffExchangeRequest(BaseModel):
    handoff_token: str = Field(min_length=16, max_length=128)


@router.post("/console-handoff")
def create_console_handoff(user: auth.DeskUser = Depends(auth.get_current_user)):
    """Staff já autenticado no Desk — token único para abrir Console /admin sem re-login."""
    token, expires_in = console_handoff.create_handoff(user.username, user.role)
    return {"handoff_token": token, "expires_in": expires_in}


@router.post("/console-handoff/exchange")
def exchange_console_handoff(body: ConsoleHandoffExchangeRequest, request: Request):
    """Console SPA troca handoff por JWT Desk (mesmo emissor que login)."""
    client_ip = request.client.host if request.client else "unknown"
    auth.check_login_rate_limit(client_ip)
    rec = console_handoff.consume_handoff(body.handoff_token)
    if not rec:
        raise HTTPException(401, "handoff inválido ou expirado")
    row = auth._user_row(rec["username"])
    if not row or not row["active"]:
        raise HTTPException(401, "utilizador inactivo")
    user = auth.DeskUser(
        username=row["username"],
        role=row["role"],
        display_name=row["display_name"],
        active=True,
    )
    access_token, expires_in = auth.create_access_token(user)
    return {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": expires_in,
        "username": user.username,
        "role": user.role,
        "display_name": user.display_name,
    }
