#!/usr/bin/env python3
"""
Auth Blueprint — JWT authentication + multi-tenant plan enforcement
Sprint 2-3: HRT-28

Endpoints:
  POST /api/v1/auth/register  — email/password registration
  POST /api/v1/auth/login     — returns access_token (15min) + refresh_token (30d)
  POST /api/v1/auth/refresh   — rotate refresh token, issue new access_token
  POST /api/v1/auth/logout    — revoke refresh token

Middleware exposed:
  jwt_required_middleware()   — decorator: valid access JWT required
  plan_required(plans)        — decorator: user plan must be in given list
"""

import os
import hashlib
import secrets
import logging
from datetime import datetime, timedelta, timezone
from functools import wraps

import bcrypt
from flask import Blueprint, request, jsonify, g, current_app
from flask_jwt_extended import (
    JWTManager,
    create_access_token,
    create_refresh_token,
    decode_token,
    get_jwt_identity,
    verify_jwt_in_request,
)
from flask_jwt_extended.exceptions import JWTExtendedException
from jwt.exceptions import PyJWTError

from auth_db import get_db

logger = logging.getLogger("turf_saas.auth")

auth_bp = Blueprint("auth", __name__, url_prefix="/api/v1/auth")

# ──────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────


def _hash_token(raw_token: str) -> str:
    """SHA-256 hash of a token string for secure DB storage."""
    return hashlib.sha256(raw_token.encode()).hexdigest()


def _get_user_by_email(email: str):
    db = get_db()
    user = db.execute(
        "SELECT * FROM users WHERE email = ? AND is_active = 1", (email.lower(),)
    ).fetchone()
    db.close()
    return user


def _get_user_by_id(user_id: int):
    db = get_db()
    user = db.execute(
        "SELECT * FROM users WHERE id = ? AND is_active = 1", (user_id,)
    ).fetchone()
    db.close()
    return user


def _store_refresh_token(user_id: int, raw_token: str, expires_at: datetime):
    token_hash = _hash_token(raw_token)
    db = get_db()
    db.execute(
        "INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES (?,?,?)",
        (user_id, token_hash, expires_at.isoformat()),
    )
    db.commit()
    db.close()


def _revoke_refresh_token(raw_token: str):
    token_hash = _hash_token(raw_token)
    db = get_db()
    db.execute(
        "UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?", (token_hash,)
    )
    db.commit()
    db.close()


def _is_refresh_token_valid(raw_token: str, user_id: int) -> bool:
    token_hash = _hash_token(raw_token)
    db = get_db()
    row = db.execute(
        """SELECT id FROM refresh_tokens
           WHERE token_hash = ? AND user_id = ? AND revoked = 0
             AND expires_at > datetime('now')""",
        (token_hash, user_id),
    ).fetchone()
    db.close()
    return row is not None


# ──────────────────────────────────────────────────────────────
# Auth endpoints
# ──────────────────────────────────────────────────────────────


@auth_bp.route("/register", methods=["POST"])
def register():
    """POST /api/v1/auth/register — create a new user account (plan=free)."""
    data = request.get_json(silent=True) or {}
    email = (data.get("email") or "").strip().lower()
    password = data.get("password") or ""

    if not email or "@" not in email:
        return jsonify({"error": "Email invalide"}), 400
    if len(password) < 8:
        return jsonify({"error": "Mot de passe trop court (min 8 caractères)"}), 400

    # Check uniqueness
    existing = _get_user_by_email(email)
    if existing:
        return jsonify({"error": "Email déjà enregistré"}), 409

    password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

    db = get_db()
    try:
        cursor = db.execute(
            "INSERT INTO users (email, password_hash, plan) VALUES (?,?,?)",
            (email, password_hash, "free"),
        )
        user_id = cursor.lastrowid
        # Create initial subscription record
        db.execute(
            "INSERT INTO subscriptions (user_id, plan) VALUES (?,?)",
            (user_id, "free"),
        )
        db.commit()
    except Exception as e:
        db.rollback()
        logger.error("register error: %s", e)
        return jsonify({"error": "Erreur interne"}), 500
    finally:
        db.close()

    logger.info("New user registered: %s (id=%s)", email, user_id)
    return jsonify({"message": "Compte créé avec succès", "user_id": user_id}), 201


@auth_bp.route("/login", methods=["POST"])
def login():
    """POST /api/v1/auth/login — returns JWT access_token + refresh_token."""
    data = request.get_json(silent=True) or {}
    email = (data.get("email") or "").strip().lower()
    password = data.get("password") or ""

    if not email or not password:
        return jsonify({"error": "Email et mot de passe requis"}), 400

    user = _get_user_by_email(email)
    if not user:
        return jsonify({"error": "Identifiants invalides"}), 401

    if not bcrypt.checkpw(password.encode(), user["password_hash"].encode()):
        logger.warning("Failed login attempt for %s", email)
        return jsonify({"error": "Identifiants invalides"}), 401

    # Create tokens
    identity = str(user["id"])
    additional_claims = {"plan": user["plan"], "email": user["email"]}

    access_token = create_access_token(
        identity=identity,
        additional_claims=additional_claims,
    )
    raw_refresh = create_refresh_token(identity=identity)

    refresh_expires = datetime.now(timezone.utc) + timedelta(days=30)
    _store_refresh_token(user["id"], raw_refresh, refresh_expires)

    logger.info("User %s logged in (plan=%s)", email, user["plan"])
    return jsonify(
        {
            "access_token": access_token,
            "refresh_token": raw_refresh,
            "token_type": "Bearer",
            "plan": user["plan"],
        }
    ), 200


@auth_bp.route("/refresh", methods=["POST"])
def refresh():
    """POST /api/v1/auth/refresh — rotate refresh token, issue new access_token."""
    data = request.get_json(silent=True) or {}
    raw_refresh = (data.get("refresh_token") or "").strip()

    if not raw_refresh:
        return jsonify({"error": "refresh_token manquant"}), 400

    # Decode without verifying in DB first (to get user_id)
    try:
        decoded = decode_token(raw_refresh)
    except Exception:
        return jsonify({"error": "Refresh token invalide ou expiré"}), 401

    user_id = int(decoded.get("sub", 0))

    if not _is_refresh_token_valid(raw_refresh, user_id):
        return jsonify({"error": "Refresh token invalide, révoqué ou expiré"}), 401

    user = _get_user_by_id(user_id)
    if not user:
        return jsonify({"error": "Utilisateur introuvable"}), 401

    # Revoke old refresh token (rotation)
    _revoke_refresh_token(raw_refresh)

    # Issue new tokens
    identity = str(user["id"])
    additional_claims = {"plan": user["plan"], "email": user["email"]}
    new_access = create_access_token(
        identity=identity, additional_claims=additional_claims
    )
    new_refresh = create_refresh_token(identity=identity)

    refresh_expires = datetime.now(timezone.utc) + timedelta(days=30)
    _store_refresh_token(user["id"], new_refresh, refresh_expires)

    logger.info("Token refreshed for user_id=%s", user_id)
    return jsonify(
        {
            "access_token": new_access,
            "refresh_token": new_refresh,
            "token_type": "Bearer",
            "plan": user["plan"],
        }
    ), 200


@auth_bp.route("/logout", methods=["POST"])
def logout():
    """POST /api/v1/auth/logout — revoke refresh token."""
    data = request.get_json(silent=True) or {}
    raw_refresh = (data.get("refresh_token") or "").strip()

    if raw_refresh:
        _revoke_refresh_token(raw_refresh)

    return jsonify({"message": "Déconnexion réussie"}), 200


# ──────────────────────────────────────────────────────────────
# JWT-protected middleware
# ──────────────────────────────────────────────────────────────


def jwt_required_middleware(fn):
    """Decorator: require a valid Bearer JWT access token."""

    @wraps(fn)
    def wrapper(*args, **kwargs):
        try:
            verify_jwt_in_request()
            user_id = int(get_jwt_identity())
            user = _get_user_by_id(user_id)
            if not user:
                return jsonify({"error": "Utilisateur introuvable"}), 401
            g.current_user = dict(user)
            g.current_user_id = user_id
        except (JWTExtendedException, PyJWTError) as e:
            logger.debug("JWT auth failed: %s", e)
            return jsonify({"error": "Token invalide ou expiré", "detail": str(e)}), 401
        return fn(*args, **kwargs)

    return wrapper


def plan_required(*allowed_plans):
    """
    Decorator factory: user's plan must be in allowed_plans.
    Must be applied AFTER @jwt_required_middleware.

    Example:
        @app.route("/api/v1/predictions")
        @jwt_required_middleware
        @plan_required("premium", "pro")
        def premium_predictions():
            ...
    """

    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            user = getattr(g, "current_user", None)
            if not user:
                return jsonify({"error": "Non authentifié"}), 401
            if user["plan"] not in allowed_plans:
                return jsonify(
                    {
                        "error": "Plan insuffisant",
                        "required": list(allowed_plans),
                        "current_plan": user["plan"],
                        "upgrade_url": "/api/v1/subscription/upgrade",
                    }
                ), 403
            return fn(*args, **kwargs)

        return wrapper

    return decorator


def free_daily_limit_check(fn):
    """
    Decorator: enforce free plan daily limit (1 course/jour).
    Must be applied AFTER @jwt_required_middleware.
    """

    @wraps(fn)
    def wrapper(*args, **kwargs):
        user = getattr(g, "current_user", None)
        if not user or user["plan"] != "free":
            return fn(*args, **kwargs)

        today = datetime.now(timezone.utc).date().isoformat()
        db = get_db()
        row = db.execute(
            "SELECT daily_usage, last_usage_date FROM users WHERE id = ?",
            (user["id"],),
        ).fetchone()
        db.close()

        if row and row["last_usage_date"] == today and row["daily_usage"] >= 1:
            return jsonify(
                {
                    "error": "Limite quotidienne atteinte (plan free: 1 course/jour)",
                    "upgrade_url": "/api/v1/subscription/upgrade",
                }
            ), 429

        # Increment usage
        db = get_db()
        if row and row["last_usage_date"] == today:
            db.execute(
                "UPDATE users SET daily_usage = daily_usage + 1 WHERE id = ?",
                (user["id"],),
            )
        else:
            db.execute(
                "UPDATE users SET daily_usage = 1, last_usage_date = ? WHERE id = ?",
                (today, user["id"]),
            )
        db.commit()
        db.close()

        return fn(*args, **kwargs)

    return wrapper
