"""Comptes utilisateurs + OAuth (Google / GitHub / GitLab).

Les utilisateurs sont stockés en MySQL si disponible, sinon repli fichier JSON.
Les mots de passe sont hachés avec PBKDF2-HMAC-SHA256 (stdlib, sel aléatoire).

OAuth : délégué à request + os.environ (client_id/secret du .env de prod).
"""
import base64
import hashlib
import hmac
import json
import os
import secrets
from pathlib import Path
from typing import Optional
from urllib.parse import urlencode

import requests

BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = BASE_DIR / "app" / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)

# ---------------------------------------------------------------------------
# Store utilisateurs (MySQL optionnel, repli fichier)
# ---------------------------------------------------------------------------
def _mysql_conn():
    if not os.getenv("DB_USER") or not os.getenv("DB_NAME"):
        return None
    try:
        import pymysql
        import socket
        old = socket.getdefaulttimeout()
        socket.setdefaulttimeout(3)
        try:
            conn = pymysql.connect(
                host=os.getenv("DB_HOST", "localhost"),
                user=os.getenv("DB_USER", ""),
                password=os.getenv("DB_PASSWORD", ""),
                database=os.getenv("DB_NAME", ""),
                charset="utf8mb4",
                cursorclass=pymysql.cursors.DictCursor,
                connect_timeout=3,
                read_timeout=5,
            )
        finally:
            socket.setdefaulttimeout(old)
        return conn
    except Exception:
        return None


def init_users_table():
    conn = _mysql_conn()
    if conn is None:
        return
    try:
        with conn.cursor() as cur:
            cur.execute("""
                CREATE TABLE IF NOT EXISTS users (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    username VARCHAR(64) NOT NULL UNIQUE,
                    email VARCHAR(255),
                    password_hash TEXT,
                    provider VARCHAR(16) DEFAULT 'local',
                    provider_id VARCHAR(128),
                    avatar TEXT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    UNIQUE KEY uq_provider (provider, provider_id)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
            """)
        conn.commit()
    finally:
        conn.close()


def _users_file() -> Path:
    return DATA_DIR / "users.json"


def _load_users() -> dict:
    p = _users_file()
    if p.exists():
        try:
            return json.loads(p.read_text())
        except Exception:
            return {}
    return {}


def _save_users(data: dict):
    _users_file().write_text(json.dumps(data, indent=2))


def _hash_password(pw: str) -> str:
    salt = secrets.token_bytes(16)
    dk = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt, 120_000)
    return "pbkdf2$" + base64.b64encode(salt).decode() + "$" + base64.b64encode(dk).decode()


def _verify_password(pw: str, stored: str) -> bool:
    try:
        algo, salt_b64, dk_b64 = stored.split("$")
        salt = base64.b64decode(salt_b64)
        dk = base64.b64decode(dk_b64)
        calc = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt, 120_000)
        return hmac.compare_digest(calc, dk)
    except Exception:
        return False


def find_user(username: str):
    conn = _mysql_conn()
    if conn is not None:
        try:
            with conn.cursor() as cur:
                cur.execute("SELECT * FROM users WHERE username=%s", (username,))
                return cur.fetchone()
        finally:
            conn.close()
    return _load_users().get(username)


def find_by_oauth(provider: str, provider_id: str):
    conn = _mysql_conn()
    if conn is not None:
        try:
            with conn.cursor() as cur:
                cur.execute("SELECT * FROM users WHERE provider=%s AND provider_id=%s",
                            (provider, str(provider_id)))
                return cur.fetchone()
        finally:
            conn.close()
    for u in _load_users().values():
        if u.get("provider") == provider and str(u.get("provider_id")) == str(provider_id):
            return u
    return None


def create_local_user(username: str, email: Optional[str], password: str) -> dict:
    rec = {
        "username": username, "email": email,
        "password_hash": _hash_password(password),
        "provider": "local", "provider_id": None, "avatar": None,
    }
    conn = _mysql_conn()
    if conn is not None:
        try:
            with conn.cursor() as cur:
                cur.execute(
                    "INSERT IGNORE INTO users (username,email,password_hash,provider) "
                    "VALUES (%s,%s,%s,'local')",
                    (username, email, rec["password_hash"]))
            conn.commit()
            return find_user(username) or rec
        finally:
            conn.close()
    data = _load_users()
    data[username] = rec
    _save_users(data)
    return rec


def verify_local(username: str, password: str) -> Optional[dict]:
    u = find_user(username)
    if not u:
        return None
    if u.get("provider") != "local":
        return None
    if _verify_password(password, u["password_hash"]):
        return u
    return None


def upsert_oauth_user(provider: str, provider_id, username, email, avatar) -> dict:
    rec = {
        "username": username, "email": email,
        "password_hash": None,
        "provider": provider, "provider_id": str(provider_id), "avatar": avatar,
    }
    conn = _mysql_conn()
    if conn is not None:
        try:
            with conn.cursor() as cur:
                cur.execute(
                    """INSERT INTO users (username,email,password_hash,provider,provider_id,avatar)
                       VALUES (%s,%s,NULL,%s,%s,%s)
                       ON DUPLICATE KEY UPDATE email=COALESCE(VALUES(email),email),
                       avatar=COALESCE(VALUES(avatar),avatar)""",
                    (username, email, provider, str(provider_id), avatar))
            conn.commit()
            return find_by_oauth(provider, provider_id)
        finally:
            conn.close()
    data = _load_users()
    # associer via provider+provider_id ; username unique
    key = f"{provider}:{provider_id}"
    if key not in data:
        # éviter collision username
        base = username or key
        cand = base
        n = 1
        while any(v.get("username") == cand for v in data.values()):
            cand = f"{base}_{n}"
            n += 1
        data[key] = {**rec, "username": cand}
        _save_users(data)
    return data.get(key)


def list_username_taken(username: str) -> bool:
    conn = _mysql_conn()
    if conn is not None:
        try:
            with conn.cursor() as cur:
                cur.execute("SELECT 1 FROM users WHERE username=%s", (username,))
                return cur.fetchone() is not None
        finally:
            conn.close()
    return any(v.get("username") == username for v in _load_users().values())


# ---------------------------------------------------------------------------
# OAuth configuraation (depuis .env)
# ---------------------------------------------------------------------------
def oauth_config():
    return {
        "google": {
            "client_id": os.getenv("OAUTH_GOOGLE_ID", ""),
            "client_secret": os.getenv("OAUTH_GOOGLE_SECRET", ""),
            "auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
            "token_url": "https://oauth2.googleapis.com/token",
            "userinfo_url": "https://www.googleapis.com/oauth2/v3/userinfo",
            "scope": "openid email profile",
        },
        "github": {
            "client_id": os.getenv("OAUTH_GITHUB_ID", ""),
            "client_secret": os.getenv("OAUTH_GITHUB_SECRET", ""),
            "auth_url": "https://github.com/login/oauth/authorize",
            "token_url": "https://github.com/login/oauth/access_token",
            "userinfo_url": "https://api.github.com/user",
            "scope": "read:user user:email",
        },
        "gitlab": {
            "client_id": os.getenv("OAUTH_GITLAB_ID", ""),
            "client_secret": os.getenv("OAUTH_GITLAB_SECRET", ""),
            "auth_url": "https://gitlab.com/oauth/authorize",
            "token_url": "https://gitlab.com/oauth/token",
            "userinfo_url": "https://gitlab.com/api/v4/user",
            "scope": "read_user",
        },
    }


def get_redirect_uri(provider: str, base_url: str) -> str:
    return f"{base_url}/v1/auth/{provider}/callback"


def build_authorize_url(provider: str, base_url: str, state: str) -> str:
    c = oauth_config()[provider]
    params = {
        "client_id": c["client_id"],
        "redirect_uri": get_redirect_uri(provider, base_url),
        "response_type": "code",
        "scope": c["scope"],
        "state": state,
    }
    return c["auth_url"] + "?" + urlencode(params)


def exchange_code(provider: str, code: str, base_url: str):
    """Échange le code contre un token. Retourne le profil utilisateur dict."""
    c = oauth_config()[provider]
    data = {
        "client_id": c["client_id"],
        "client_secret": c["client_secret"],
        "code": code,
        "grant_type": "authorization_code",
        "redirect_uri": get_redirect_uri(provider, base_url),
    }
    headers = {"Accept": "application/json"}
    if provider == "github":
        headers["Accept"] = "application/json"
    r = requests.post(c["token_url"], data=data, headers=headers, timeout=30)
    if r.status_code != 200:
        raise RuntimeError(f"Token KO ({provider} {r.status_code}): {r.text[:200]}")
    tok = r.json()
    access = tok.get("access_token")
    if not access:
        raise RuntimeError(f"Pas de access_token ({provider})")
    return get_profile(provider, access)


def get_profile(provider: str, access_token: str) -> dict:
    c = oauth_config()[provider]
    headers = {"Authorization": f"Bearer {access_token}"}
    if provider == "github":
        headers["Accept"] = "application/json"
        headers["X-GitHub-Api-Version"] = "2022-11-28"
    r = requests.get(c["userinfo_url"], headers=headers, timeout=30)
    if r.status_code != 200:
        raise RuntimeError(f"Profil KO ({provider} {r.status_code})")
    prof = r.json()

    if provider == "google":
        return {
            "provider_id": prof.get("sub"),
            "email": prof.get("email"),
            "username": (prof.get("email") or "user").split("@")[0],
            "avatar": prof.get("picture"),
        }
    if provider == "github":
        return {
            "provider_id": str(prof.get("id")),
            "email": prof.get("email"),
            "username": prof.get("login") or f"gh_{prof.get('id')}",
            "avatar": prof.get("avatar_url"),
        }
    if provider == "gitlab":
        return {
            "provider_id": str(prof.get("id")),
            "email": prof.get("email"),
            "username": prof.get("username") or f"gl_{prof.get('id')}",
            "avatar": prof.get("avatar_url"),
        }
    raise RuntimeError("Provider inconnu")


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


def verify_state(state: str, expected: str) -> bool:
    return bool(state and expected and hmac.compare_digest(state, expected))
