"""Auth: bcrypt password hashing, signed session cookies, reset/setup tokens.

Mirrors tracking-setup/app/auth.py conventions.
"""

import hashlib
import os
import secrets
import time
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Optional

import bcrypt
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer

from db import execute, query_one

SECRET_KEY = os.environ.get("SECRET_KEY", "")
if not SECRET_KEY:
    raise RuntimeError("SECRET_KEY env var is required")

SESSION_COOKIE = "ss_session"
CSRF_COOKIE = "ss_csrf"
VIEWAS_COOKIE = "ss_viewas"
SESSION_TTL_DAYS = 30
RESET_TTL_HOURS = 48
SETUP_TTL_DAYS = 14

_session_signer = URLSafeTimedSerializer(SECRET_KEY, salt="ss-session-v1")
_viewas_signer = URLSafeTimedSerializer(SECRET_KEY, salt="ss-viewas-v1")

ROLES = ("admin", "manager", "client")


@dataclass
class CurrentUser:
    id: int
    email: str
    name: Optional[str]
    role: str  # 'admin' | 'client'

    @property
    def is_admin(self) -> bool:
        return self.role == "admin"

    @property
    def can_create(self) -> bool:
        """admin + manager may create forms/clients (via UI or the management API)."""
        return self.role in ("admin", "manager")


def hash_password(plain: str) -> str:
    return bcrypt.hashpw(plain.encode(), bcrypt.gensalt()).decode()


def verify_password(plain: str, hashed: Optional[str]) -> bool:
    if not hashed:
        return False
    try:
        return bcrypt.checkpw(plain.encode(), hashed.encode())
    except ValueError:
        return False


def create_session_cookie(client_id: int) -> str:
    return _session_signer.dumps({"uid": client_id, "iat": int(time.time())})


def read_session_cookie(token: Optional[str]) -> Optional[int]:
    if not token:
        return None
    try:
        data = _session_signer.loads(token, max_age=SESSION_TTL_DAYS * 86400)
        return int(data["uid"])
    except (BadSignature, SignatureExpired, KeyError, ValueError, TypeError):
        return None


def load_user(client_id: int) -> Optional[CurrentUser]:
    row = query_one("SELECT id, email, name, role FROM clients WHERE id = ?", (client_id,))
    if not row:
        return None
    return CurrentUser(id=row["id"], email=row["email"], name=row["name"], role=row["role"])


def authenticate(email: str, password: str) -> Optional[CurrentUser]:
    row = query_one(
        "SELECT id, email, name, role, password_hash FROM clients WHERE email = ?",
        (email.strip().lower(),),
    )
    if not row or not verify_password(password, row["password_hash"]):
        return None
    execute("UPDATE clients SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?", (row["id"],))
    return CurrentUser(id=row["id"], email=row["email"], name=row["name"], role=row["role"])


def issue_token(client_id: int, purpose: str = "reset") -> str:
    token = secrets.token_urlsafe(32)
    ttl = timedelta(days=SETUP_TTL_DAYS) if purpose == "setup" else timedelta(hours=RESET_TTL_HOURS)
    expires_at = datetime.now(timezone.utc) + ttl
    execute(
        "INSERT INTO password_reset (token, client_id, purpose, expires_at) VALUES (?, ?, ?, ?)",
        (token, client_id, purpose, expires_at),
    )
    return token


def peek_token(token: str) -> Optional[dict]:
    """Validate a reset/setup token without consuming it (for showing the form)."""
    row = query_one(
        "SELECT token, client_id, purpose, expires_at, used_at FROM password_reset WHERE token = ?",
        (token,),
    )
    if not row or row["used_at"] is not None:
        return None
    expires_at = row["expires_at"]
    if isinstance(expires_at, str):
        expires_at = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
    if expires_at.tzinfo is None:
        expires_at = expires_at.replace(tzinfo=timezone.utc)
    if expires_at < datetime.now(timezone.utc):
        return None
    return {"client_id": row["client_id"], "purpose": row["purpose"]}


def consume_token(token: str, new_password: str) -> Optional[int]:
    """Validate + burn a token, set the new password. Returns client_id on success."""
    info = peek_token(token)
    if not info:
        return None
    execute(
        "UPDATE clients SET password_hash = ? WHERE id = ?",
        (hash_password(new_password), info["client_id"]),
    )
    execute("UPDATE password_reset SET used_at = CURRENT_TIMESTAMP WHERE token = ?", (token,))
    return info["client_id"]


# ---- CSRF (double-submit cookie) ----

def new_csrf_token() -> str:
    return secrets.token_urlsafe(24)


def csrf_ok(cookie_val: Optional[str], form_val: Optional[str]) -> bool:
    return bool(cookie_val) and bool(form_val) and secrets.compare_digest(cookie_val, form_val)


# ---- API tokens (per-user, for the management CLI) ----

def new_api_token() -> str:
    """Plaintext token shown once. Prefixed for recognisability."""
    return "sk_" + secrets.token_urlsafe(32)


def hash_token(plain: str) -> str:
    return hashlib.sha256(plain.encode()).hexdigest()


def load_user_by_token(plain: str) -> Optional[CurrentUser]:
    if not plain:
        return None
    row = query_one(
        """SELECT c.id, c.email, c.name, c.role, t.id AS tok_id
           FROM api_tokens t JOIN clients c ON c.id = t.client_id
           WHERE t.token_hash = ? AND t.revoked = 0""",
        (hash_token(plain),),
    )
    if not row:
        return None
    execute("UPDATE api_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?", (row["tok_id"],))
    return CurrentUser(id=row["id"], email=row["email"], name=row["name"], role=row["role"])


# ---- View-as (admin impersonation) ----

def make_viewas_cookie(client_id: int) -> str:
    return _viewas_signer.dumps({"cid": client_id})


def read_viewas_cookie(token: Optional[str]) -> Optional[int]:
    if not token:
        return None
    try:
        return int(_viewas_signer.loads(token, max_age=86400)["cid"])
    except (BadSignature, SignatureExpired, KeyError, ValueError, TypeError):
        return None
