"""
SAYTHON Framework - Authentication
Built-in User model + JWT tokens + @auth_required decorator.

Usage:
    from saython.auth import auth_required, get_auth_router

    # Protect any endpoint:
    @app.get("/me")
    @auth_required
    def me(req):
        return req.user.to_dict(exclude=["password"])

    # Mount built-in auth routes (/auth/register, /auth/login, /auth/me):
    app.include("/auth", get_auth_router())
"""
import hmac
import hashlib
import base64
import json
import os
import time
import functools
from typing import Optional

from saython.orm import Model, Field
from saython.exceptions import Unauthorized, BadRequest, ValidationError


# ------------------------------------------------------------------ #
#  Config (set via app.config or env vars)
# ------------------------------------------------------------------ #
_SECRET_KEY: str = os.environ.get("SECRET_KEY", "saython-secret-change-me-in-production")
_TOKEN_EXPIRY: int = int(os.environ.get("TOKEN_EXPIRY_HOURS", "24")) * 3600   # seconds


def set_secret_key(key: str):
    global _SECRET_KEY
    _SECRET_KEY = key


def set_token_expiry(hours: int):
    global _TOKEN_EXPIRY
    _TOKEN_EXPIRY = hours * 3600


# ------------------------------------------------------------------ #
#  Password hashing  (bcrypt if available, else PBKDF2-HMAC-SHA256)
# ------------------------------------------------------------------ #
def _hash_password(plain: str) -> str:
    """Hash a plain-text password. Returns a storable string."""
    try:
        import bcrypt  # type: ignore
        return bcrypt.hashpw(plain.encode(), bcrypt.gensalt()).decode()
    except ImportError:
        salt = os.urandom(16)
        dk = hashlib.pbkdf2_hmac("sha256", plain.encode(), salt, 260_000)
        return "pbkdf2$" + base64.b64encode(salt + dk).decode()


def _verify_password(plain: str, hashed: str) -> bool:
    try:
        import bcrypt  # type: ignore
        return bcrypt.checkpw(plain.encode(), hashed.encode())
    except ImportError:
        if not hashed.startswith("pbkdf2$"):
            return False
        raw = base64.b64decode(hashed[7:])
        salt, stored_dk = raw[:16], raw[16:]
        dk = hashlib.pbkdf2_hmac("sha256", plain.encode(), salt, 260_000)
        return hmac.compare_digest(dk, stored_dk)


# ------------------------------------------------------------------ #
#  Minimal JWT  (uses PyJWT if available, else pure-Python HS256)
# ------------------------------------------------------------------ #
def _b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()


def _b64url_decode(s: str) -> bytes:
    s += "=" * (-len(s) % 4)
    return base64.urlsafe_b64decode(s)


def _create_token(payload: dict) -> str:
    try:
        import jwt  # type: ignore
        payload["exp"] = int(time.time()) + _TOKEN_EXPIRY
        return jwt.encode(payload, _SECRET_KEY, algorithm="HS256")
    except ImportError:
        pass

    # Pure-Python HS256
    header = _b64url_encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
    payload["exp"] = int(time.time()) + _TOKEN_EXPIRY
    body = _b64url_encode(json.dumps(payload).encode())
    signing_input = f"{header}.{body}".encode()
    sig = hmac.new(_SECRET_KEY.encode(), signing_input, hashlib.sha256).digest()
    return f"{header}.{body}.{_b64url_encode(sig)}"


def _decode_token(token: str) -> dict:
    try:
        import jwt  # type: ignore
        return jwt.decode(token, _SECRET_KEY, algorithms=["HS256"])
    except ImportError:
        pass
    except Exception as e:
        raise Unauthorized(f"Invalid token: {e}")

    # Pure-Python verification
    try:
        parts = token.split(".")
        if len(parts) != 3:
            raise Unauthorized("Malformed token")
        header, body, sig = parts
        signing_input = f"{header}.{body}".encode()
        expected_sig = hmac.new(_SECRET_KEY.encode(), signing_input, hashlib.sha256).digest()
        if not hmac.compare_digest(expected_sig, _b64url_decode(sig)):
            raise Unauthorized("Token signature invalid")
        payload = json.loads(_b64url_decode(body))
        if "exp" in payload and time.time() > payload["exp"]:
            raise Unauthorized("Token has expired")
        return payload
    except Unauthorized:
        raise
    except Exception:
        raise Unauthorized("Token decode error")


# ------------------------------------------------------------------ #
#  Built-in User model
# ------------------------------------------------------------------ #
class User(Model):
    """
    Built-in user model.

    Fields: id, username, email, password (hashed), is_active, is_admin,
            created_at, updated_at
    """
    username  = Field(str,  required=True, unique=True)
    email     = Field(str,  required=True, unique=True)
    password  = Field(str,  required=True)
    is_active = Field(bool, default=True)
    is_admin  = Field(bool, default=False)

    class Meta:
        table_name = "users"

    @classmethod
    def register(cls, username: str, email: str, password: str, **extra) -> "User":
        """Create a new user with a hashed password."""
        cls._create_table()

        errors = {}
        if not username or len(username) < 3:
            errors["username"] = "At least 3 characters required"
        if not email or "@" not in email:
            errors["email"] = "Valid email required"
        if not password or len(password) < 6:
            errors["password"] = "At least 6 characters required"
        if cls.exists(username=username):
            errors["username"] = "Username already taken"
        if cls.exists(email=email):
            errors["email"] = "Email already registered"
        if errors:
            raise ValidationError(errors)

        hashed = _hash_password(password)
        return cls.create(username=username, email=email, password=hashed, **extra)

    @classmethod
    def authenticate(cls, username_or_email: str, password: str) -> "User":
        """Verify credentials. Returns User or raises Unauthorized."""
        user = cls.first(username=username_or_email) or cls.first(email=username_or_email)
        if not user:
            raise Unauthorized("Invalid credentials")
        if not _verify_password(password, user.password):
            raise Unauthorized("Invalid credentials")
        if not user.is_active:
            raise Unauthorized("Account is disabled")
        return user

    def generate_token(self) -> str:
        """Generate a JWT for this user."""
        return _create_token({"user_id": self.id, "username": self.username})

    def to_dict(self, exclude: list = None) -> dict:
        exclude = list(exclude or []) + ["password"]
        return super().to_dict(exclude=exclude)

    def __repr__(self):
        return f"<User id={self.id} username={self.username}>"


# ------------------------------------------------------------------ #
#  @auth_required decorator
# ------------------------------------------------------------------ #
def auth_required(fn):
    """
    Decorator that verifies JWT and injects req.user.

    Usage:
        @app.get("/dashboard")
        @auth_required
        def dashboard(req):
            return {"hello": req.user.username}
    """
    @functools.wraps(fn)
    def wrapper(req, *args, **kwargs):
        token = req.auth_token
        if not token:
            raise Unauthorized("Authentication required. Send: Authorization: Bearer <token>")
        payload = _decode_token(token)
        user_id = payload.get("user_id")
        if not user_id:
            raise Unauthorized("Invalid token payload")
        try:
            user = User.get(user_id)
        except Exception:
            raise Unauthorized("User not found")
        if not user.is_active:
            raise Unauthorized("Account is disabled")
        req.user = user
        return fn(req, *args, **kwargs)
    return wrapper


def admin_required(fn):
    """Like @auth_required but also checks is_admin=True."""
    @functools.wraps(fn)
    def wrapper(req, *args, **kwargs):
        auth_required(lambda r, *a, **kw: None)(req)   # run auth check first
        # Re-attach user by calling auth_required inline
        token = req.auth_token
        payload = _decode_token(token)
        user = User.get(payload["user_id"])
        req.user = user
        if not user.is_admin:
            raise Unauthorized("Admin access required")
        return fn(req, *args, **kwargs)
    return wrapper


# ------------------------------------------------------------------ #
#  Built-in auth router
# ------------------------------------------------------------------ #
def get_auth_router():
    """
    Returns a Router with built-in auth endpoints.
    Mount it with: app.include("/auth", get_auth_router())

    Endpoints:
        POST /auth/register  → { id, username, email, token }
        POST /auth/login     → { token, user }
        GET  /auth/me        → current user info  (requires token)
        PUT  /auth/me        → update email       (requires token)
    """
    from saython.router import Router
    router = Router()

    @router.post("/register")
    def register(req):
        from saython.response import Response
        data = req.json
        user = User.register(
            username=data.get("username", ""),
            email=data.get("email", ""),
            password=data.get("password", ""),
        )
        token = user.generate_token()
        return Response({**user.to_dict(), "token": token}, 201)

    @router.post("/login")
    def login(req):
        from saython.response import Response
        data = req.json
        user = User.authenticate(
            data.get("username") or data.get("email", ""),
            data.get("password", ""),
        )
        return Response({"token": user.generate_token(), "user": user.to_dict()})

    @router.get("/me")
    @auth_required
    def me(req):
        return req.user.to_dict()

    @router.put("/me")
    @auth_required
    def update_me(req):
        data = req.json
        if "email" in data:
            req.user.email = data["email"]
        if "username" in data:
            req.user.username = data["username"]
        req.user.save()
        return req.user.to_dict()

    return router
