"""
Multi-user authentication, SQLite-backed.

- `users` table: email/password_hash/role. role is 'admin' or 'member'.
  Admins can manage other users; members can use the cleaning dashboard but
  not the user-management screen.
- `sessions` table: opaque token -> email + expiry. Sessions are looked up
  fresh against `users` on every request, so a role change (or account
  deletion) takes effect immediately without waiting for re-login.
- Passwords: PBKDF2-HMAC-SHA256, stdlib only.
- A short-lived sqlite3 connection is opened per call rather than shared
  across threads — simplest safe pattern for a small self-hosted app; if
  this app grows into something with real concurrent write load, swap for
  a connection pool (or Postgres) using the same function signatures here.
"""

import hashlib
import hmac
import re
import secrets
import sqlite3
import threading
from datetime import datetime, timedelta, timezone
from pathlib import Path

DATA_DIR = Path(__file__).parent / "data"
DATA_DIR.mkdir(exist_ok=True)
DB_PATH = DATA_DIR / "app.db"

SESSION_TTL_HOURS = 24 * 7  # 1 week
PBKDF2_ITERATIONS = 260_000
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
VALID_ROLES = ("admin", "member")

_write_lock = threading.Lock()


def _connect() -> sqlite3.Connection:
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn


def init_db():
    with _write_lock, _connect() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                email TEXT UNIQUE NOT NULL,
                password_hash TEXT NOT NULL,
                role TEXT NOT NULL CHECK(role IN ('admin','member')),
                created_at TEXT NOT NULL
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS sessions (
                token TEXT PRIMARY KEY,
                email TEXT NOT NULL,
                expires_at TEXT NOT NULL
            )
        """)
        conn.commit()


init_db()


# ---------------------------------------------------------------------------
# Password hashing / validation
# ---------------------------------------------------------------------------

def hash_password(password: str) -> str:
    salt = secrets.token_hex(16)
    dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), bytes.fromhex(salt), PBKDF2_ITERATIONS)
    return f"{salt}${dk.hex()}"


def verify_password(password: str, stored: str) -> bool:
    try:
        salt, hash_hex = stored.split("$", 1)
    except ValueError:
        return False
    dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), bytes.fromhex(salt), PBKDF2_ITERATIONS)
    return hmac.compare_digest(dk.hex(), hash_hex)


def validate_email(email: str) -> bool:
    return bool(EMAIL_RE.match(email or ""))


def validate_password_strength(password: str) -> str | None:
    if not password or len(password) < 8:
        return "Password must be at least 8 characters"
    return None


# ---------------------------------------------------------------------------
# User management
# ---------------------------------------------------------------------------

def _row_to_user(row: sqlite3.Row) -> dict:
    return {"id": row["id"], "email": row["email"], "role": row["role"], "created_at": row["created_at"]}


def is_setup_complete() -> bool:
    with _connect() as conn:
        return conn.execute("SELECT 1 FROM users LIMIT 1").fetchone() is not None


def count_admins() -> int:
    with _connect() as conn:
        return conn.execute("SELECT COUNT(*) AS n FROM users WHERE role='admin'").fetchone()["n"]


def get_user_by_email(email: str) -> dict | None:
    with _connect() as conn:
        row = conn.execute("SELECT * FROM users WHERE email=?", (email.strip().lower(),)).fetchone()
        return _row_to_user(row) if row else None


def get_user_by_id(user_id: int) -> dict | None:
    with _connect() as conn:
        row = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone()
        return _row_to_user(row) if row else None


def list_users() -> list[dict]:
    with _connect() as conn:
        rows = conn.execute("SELECT * FROM users ORDER BY created_at ASC").fetchall()
        return [_row_to_user(r) for r in rows]


def create_user(email: str, password: str, role: str) -> dict:
    email = email.strip().lower()
    if role not in VALID_ROLES:
        raise ValueError(f"role must be one of {VALID_ROLES}")
    with _write_lock, _connect() as conn:
        existing = conn.execute("SELECT 1 FROM users WHERE email=?", (email,)).fetchone()
        if existing:
            raise ValueError("A user with that email already exists")
        now = datetime.now(timezone.utc).isoformat()
        cur = conn.execute(
            "INSERT INTO users (email, password_hash, role, created_at) VALUES (?,?,?,?)",
            (email, hash_password(password), role, now),
        )
        conn.commit()
        return {"id": cur.lastrowid, "email": email, "role": role, "created_at": now}


def authenticate(email: str, password: str) -> dict | None:
    with _connect() as conn:
        row = conn.execute("SELECT * FROM users WHERE email=?", ((email or "").strip().lower(),)).fetchone()
        if not row or not verify_password(password, row["password_hash"]):
            return None
        return _row_to_user(row)


def delete_user(user_id: int):
    with _write_lock, _connect() as conn:
        conn.execute("DELETE FROM users WHERE id=?", (user_id,))
        conn.commit()


def update_user_role(user_id: int, role: str):
    if role not in VALID_ROLES:
        raise ValueError(f"role must be one of {VALID_ROLES}")
    with _write_lock, _connect() as conn:
        conn.execute("UPDATE users SET role=? WHERE id=?", (role, user_id))
        conn.commit()


def admin_reset_password(user_id: int, new_password: str):
    with _write_lock, _connect() as conn:
        conn.execute(
            "UPDATE users SET password_hash=? WHERE id=?",
            (hash_password(new_password), user_id),
        )
        conn.commit()


def change_own_password(email: str, current_password: str, new_password: str) -> bool:
    with _write_lock, _connect() as conn:
        row = conn.execute("SELECT * FROM users WHERE email=?", (email.strip().lower(),)).fetchone()
        if not row or not verify_password(current_password, row["password_hash"]):
            return False
        conn.execute("UPDATE users SET password_hash=? WHERE id=?", (hash_password(new_password), row["id"]))
        conn.commit()
        return True


# ---------------------------------------------------------------------------
# Sessions
# ---------------------------------------------------------------------------

def create_session(email: str) -> str:
    token = secrets.token_urlsafe(32)
    expires_at = (datetime.now(timezone.utc) + timedelta(hours=SESSION_TTL_HOURS)).isoformat()
    with _write_lock, _connect() as conn:
        conn.execute("INSERT INTO sessions (token, email, expires_at) VALUES (?,?,?)", (token, email, expires_at))
        conn.commit()
    return token


def get_session_user(token: str | None) -> dict | None:
    """Returns the current {id, email, role} for a valid, unexpired session
    — looked up fresh each call so role changes/account deletion apply
    immediately, not just on next login."""
    if not token:
        return None
    with _write_lock, _connect() as conn:
        row = conn.execute("SELECT * FROM sessions WHERE token=?", (token,)).fetchone()
        if not row:
            return None
        if datetime.fromisoformat(row["expires_at"]) < datetime.now(timezone.utc):
            conn.execute("DELETE FROM sessions WHERE token=?", (token,))
            conn.commit()
            return None
        user_row = conn.execute("SELECT * FROM users WHERE email=?", (row["email"],)).fetchone()
        if not user_row:
            # user was deleted after the session was issued
            conn.execute("DELETE FROM sessions WHERE token=?", (token,))
            conn.commit()
            return None
        return _row_to_user(user_row)


def delete_session(token: str | None):
    if not token:
        return
    with _write_lock, _connect() as conn:
        conn.execute("DELETE FROM sessions WHERE token=?", (token,))
        conn.commit()
