"""Authentication and JWT for Ligbox Ops Desk."""

from __future__ import annotations

import os
import secrets
import sqlite3
import time
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from fastapi import Depends, Header, HTTPException, Request
from jose import JWTError, jwt
import bcrypt

from app.permissions import HUMAN_ROLES
from app.totp_util import verify_code as verify_totp_code

DB_PATH = Path(os.getenv("SQLITE_PATH", "/data/ops.db"))
JWT_SECRET = os.getenv("JWT_SECRET", "ligbox-ops-change-me-in-production")
JWT_ALGORITHM = "HS256"
JWT_EXPIRE_HOURS = int(os.getenv("JWT_EXPIRE_HOURS", "8"))
DESK_AUTH_ENABLED = os.getenv("DESK_AUTH_ENABLED", "true").lower() in ("1", "true", "yes")
DESK_BOOTSTRAP_PASSWORD = os.getenv("DESK_BOOTSTRAP_PASSWORD", "805353")
AUTH_LOGIN_RATE_LIMIT = int(os.getenv("AUTH_LOGIN_RATE_LIMIT", "5"))
OPS_INTERNAL_TOKEN = os.getenv("OPS_INTERNAL_TOKEN", "")

_login_attempts: dict[str, list[float]] = {}
_mfa_pending: dict[str, tuple[str, float]] = {}
MFA_TOKEN_TTL_SEC = 300

SEED_USERS = (
    ("root", "super_admin", "Roger"),
    ("admin", "ops_lead", "Chefe Ops"),
    ("mini", "technician", "Suporte"),
    ("noc", "noc", "NOC"),
)


@dataclass
class DeskUser:
    username: str
    role: str
    display_name: str | None = None
    active: bool = True

    @property
    def is_authenticated(self) -> bool:
        return bool(self.username)


def db() -> sqlite3.Connection:
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(DB_PATH, timeout=30.0, check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA busy_timeout=60000")
    return conn


def hash_password(password: str) -> str:
    return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")


def verify_password(plain: str, hashed: str) -> bool:
    if not plain or not hashed:
        return False
    try:
        return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
    except (ValueError, TypeError):
        return False


def init_auth_schema(conn: sqlite3.Connection) -> None:
    from app import backup_codes, mfa_recovery_store, registration_store

    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS desk_users (
            id INTEGER PRIMARY KEY,
            username TEXT NOT NULL UNIQUE,
            password_hash TEXT NOT NULL,
            role TEXT NOT NULL,
            display_name TEXT,
            active INTEGER NOT NULL DEFAULT 1,
            last_login_at TEXT,
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL
        )
        """
    )
    registration_store.init_registration_schema(conn)
    backup_codes.init_backup_schema(conn)
    mfa_recovery_store.init_recovery_schema(conn)
    cols = {row[1] for row in conn.execute("PRAGMA table_info(tickets)").fetchall()}
    if "assigned_to" not in cols:
        conn.execute("ALTER TABLE tickets ADD COLUMN assigned_to TEXT")
    if "assigned_at" not in cols:
        conn.execute("ALTER TABLE tickets ADD COLUMN assigned_at TEXT")

    count = conn.execute("SELECT COUNT(*) c FROM desk_users").fetchone()["c"]
    if count == 0:
        now = datetime.now(timezone.utc).isoformat()
        for username, role, display_name in SEED_USERS:
            conn.execute(
                """
                INSERT INTO desk_users
                (username, password_hash, role, display_name, active, created_at, updated_at)
                VALUES (?, ?, ?, ?, 1, ?, ?)
                """,
                (username, hash_password(DESK_BOOTSTRAP_PASSWORD), role, display_name, now, now),
            )


def create_access_token(user: DeskUser) -> tuple[str, int]:
    expires = timedelta(hours=JWT_EXPIRE_HOURS)
    expire_at = datetime.now(timezone.utc) + expires
    payload = {
        "sub": user.username,
        "role": user.role,
        "exp": expire_at,
        "iat": datetime.now(timezone.utc),
    }
    token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
    return token, int(expires.total_seconds())


def decode_token(token: str) -> DeskUser:
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
    except JWTError as exc:
        raise HTTPException(401, "invalid or expired token") from exc
    username = payload.get("sub")
    role = payload.get("role")
    if not username or role not in HUMAN_ROLES:
        raise HTTPException(401, "invalid token claims")
    with db() as conn:
        row = conn.execute(
            "SELECT username, role, display_name, active FROM desk_users WHERE username = ?",
            (username,),
        ).fetchone()
    if not row or not row["active"]:
        raise HTTPException(401, "user inactive or not found")
    return DeskUser(
        username=row["username"],
        role=row["role"],
        display_name=row["display_name"],
        active=bool(row["active"]),
    )


def _normalize_login(username: str) -> str:
    u = username.strip()
    if u.lower() == "root":
        return "root"
    if "@" in u:
        return u.lower()
    return u.lower()


def _user_row(login: str) -> sqlite3.Row | None:
    with db() as conn:
        return conn.execute(
            """
            SELECT username, password_hash, role, display_name, active, totp_secret, totp_enabled
            FROM desk_users WHERE username = ? OR email = ?
            """,
            (login, login),
        ).fetchone()


def check_credentials(username: str, password: str) -> tuple[DeskUser | None, sqlite3.Row | None]:
    login = _normalize_login(username)
    row = _user_row(login)
    if not row or not row["active"]:
        return None, None
    if not verify_password(password, row["password_hash"]):
        return None, None
    user = DeskUser(
        username=row["username"],
        role=row["role"],
        display_name=row["display_name"],
        active=True,
    )
    return user, row


def touch_last_login(username: str) -> None:
    now = datetime.now(timezone.utc).isoformat()
    for attempt in range(8):
        try:
            with db() as conn:
                conn.execute(
                    "UPDATE desk_users SET last_login_at = ?, updated_at = ? WHERE username = ?",
                    (now, now, username),
                )
                conn.commit()
            return
        except sqlite3.OperationalError as exc:
            if "locked" not in str(exc).lower() or attempt >= 7:
                raise
            time.sleep(0.25 * (attempt + 1))


def authenticate_user(username: str, password: str) -> DeskUser | None:
    user, _row = check_credentials(username, password)
    if not user:
        return None
    touch_last_login(user.username)
    return user


def user_requires_totp(row: sqlite3.Row | None) -> bool:
    if not row:
        return False
    return bool(row["totp_enabled"] and row["totp_secret"])


def create_mfa_token(username: str) -> str:
    token = secrets.token_urlsafe(32)
    _mfa_pending[token] = (username, time.time() + MFA_TOKEN_TTL_SEC)
    return token


def peek_mfa_token(token: str) -> str | None:
    entry = _mfa_pending.get(token)
    if not entry:
        return None
    username, expires = entry
    if time.time() > expires:
        _mfa_pending.pop(token, None)
        return None
    return username


def consume_mfa_token(token: str) -> str | None:
    entry = _mfa_pending.pop(token, None)
    if not entry:
        return None
    username, expires = entry
    if time.time() > expires:
        return None
    return username


def verify_user_totp(username: str, code: str) -> bool:
    row = _user_row(username)
    if not row or not row["totp_secret"]:
        return False
    return verify_totp_code(row["totp_secret"], code)


def check_login_rate_limit(client_ip: str) -> None:
    now = time.time()
    window = 60.0
    attempts = _login_attempts.setdefault(client_ip, [])
    attempts[:] = [t for t in attempts if now - t < window]
    if len(attempts) >= AUTH_LOGIN_RATE_LIMIT:
        raise HTTPException(429, "too many login attempts")
    attempts.append(now)


def _extract_bearer(authorization: str | None) -> str | None:
    if not authorization:
        return None
    parts = authorization.split(" ", 1)
    if len(parts) != 2 or parts[0].lower() != "bearer":
        return None
    return parts[1].strip() or None


def get_current_user_optional(
    authorization: str | None = Header(default=None),
) -> DeskUser | None:
    if not DESK_AUTH_ENABLED:
        return DeskUser(username="system", role="super_admin", display_name="Auth disabled")
    token = _extract_bearer(authorization)
    if not token:
        return None
    return decode_token(token)


def get_current_user(user: DeskUser | None = Depends(get_current_user_optional)) -> DeskUser:
    if not DESK_AUTH_ENABLED:
        return DeskUser(username="system", role="super_admin", display_name="Auth disabled")
    if user is None:
        raise HTTPException(401, "not authenticated")
    return user


def require_internal_or_user(
    request: Request,
    x_ops_internal_token: str | None = Header(default=None),
    user: DeskUser | None = Depends(get_current_user_optional),
) -> DeskUser:
    if OPS_INTERNAL_TOKEN and x_ops_internal_token == OPS_INTERNAL_TOKEN:
        return DeskUser(username="worker", role="super_admin", display_name="Internal")
    if not DESK_AUTH_ENABLED:
        return DeskUser(username="system", role="super_admin", display_name="Auth disabled")
    if user is None:
        raise HTTPException(401, "not authenticated")
    return user


def require_roles(*roles: str):
    allowed = frozenset(roles)

    def dependency(user: DeskUser = Depends(get_current_user)) -> DeskUser:
        if user.role not in allowed:
            raise HTTPException(403, "insufficient permissions")
        return user

    return dependency


def mask_value(value: Any) -> Any:
    if value is None:
        return None
    if isinstance(value, str):
        return "***" if value else value
    return "***"


def mask_company_profile(profile: dict | None) -> dict | None:
    if not profile:
        return profile
    masked = dict(profile)
    for key in ("tax_id", "email_billing", "email_legal", "phone_landline", "phone_mobile", "contact_phone"):
        if key in masked:
            masked[key] = "***"
    if "address" in masked:
        masked["address"] = {}
    return masked


def mask_ticket(ticket: dict) -> dict:
    out = dict(ticket)
    out["company_profile"] = mask_company_profile(out.get("company_profile"))
    out.pop("billing_state", None)
    payload = out.get("payload")
    if isinstance(payload, dict):
        payload = dict(payload)
        payload.pop("billing_state", None)
        if payload.get("company_profile"):
            payload["company_profile"] = mask_company_profile(payload["company_profile"])
        notes = payload.get("funnel_notes")
        if isinstance(notes, list):
            payload["funnel_notes"] = [
                {
                    **note,
                    "data": {
                        **(note.get("data") or {}),
                        "company_profile": mask_company_profile((note.get("data") or {}).get("company_profile")),
                    },
                }
                for note in notes
            ]
        out["payload"] = payload
    out.pop("email", None)
    return out


def mask_summary_for_noc(summary: dict) -> dict:
    out = dict(summary)
    out["recent_tickets"] = [mask_ticket(t) for t in out.get("recent_tickets", [])]
    return out


def user_public_dict(row: sqlite3.Row | dict) -> dict:
    if isinstance(row, sqlite3.Row):
        row = dict(row)
    out = {
        "username": row["username"],
        "role": row["role"],
        "display_name": row.get("display_name"),
        "active": bool(row.get("active", 1)),
        "last_login_at": row.get("last_login_at"),
        "created_at": row.get("created_at"),
        "updated_at": row.get("updated_at"),
    }
    if row.get("email"):
        out["email"] = row["email"]
    if row.get("phone"):
        out["phone"] = row["phone"]
    if "mfa_enabled" in row:
        out["mfa_enabled"] = bool(row.get("mfa_enabled"))
    if "totp_enabled" in row:
        out["totp_enabled"] = bool(row.get("totp_enabled"))
    if "backup_codes_remaining" in row:
        out["backup_codes_remaining"] = int(row.get("backup_codes_remaining") or 0)
    return out
