"""ES256 JWT access tokens + opaque refresh tokens with rotation."""
from __future__ import annotations
import hashlib
import os
import secrets
import sqlite3
from datetime import datetime, timezone, timedelta
from pathlib import Path

import jwt
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization

from .db import _connect


def generate_keypair(key_path: Path) -> None:
    """Generate ES256 private key and write to disk (mode 0600)."""
    private_key = ec.generate_private_key(ec.SECP256R1())
    pem = private_key.private_bytes(
        serialization.Encoding.PEM,
        serialization.PrivateFormat.PKCS8,
        serialization.NoEncryption(),
    )
    key_path.write_bytes(pem)
    os.chmod(key_path, 0o600)


def load_private_key(key_path: Path) -> ec.EllipticCurvePrivateKey:
    """Load ES256 private key from PEM file."""
    return serialization.load_pem_private_key(key_path.read_bytes(), password=None)


def create_access_token(key: ec.EllipticCurvePrivateKey, *, user_id: str, ttl: int) -> str:
    """Create a signed ES256 JWT access token."""
    now = datetime.now(timezone.utc)
    payload = {
        "sub": user_id,
        "iat": now,
        "exp": now + timedelta(seconds=ttl),
        "type": "access",
    }
    return jwt.encode(payload, key, algorithm="ES256")


def verify_access_token(key: ec.EllipticCurvePrivateKey, token: str) -> dict | None:
    """Verify and decode an ES256 JWT. Returns payload or None."""
    try:
        return jwt.decode(token, key.public_key(), algorithms=["ES256"])
    except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
        return None


def _hash_token(raw: str) -> str:
    return hashlib.sha256(raw.encode()).hexdigest()


def create_refresh_token(db_path: Path, *, user_id: str, ttl: int) -> str:
    """Create an opaque refresh token, store hash in DB. Returns raw token."""
    raw = "rf_" + secrets.token_urlsafe(32)
    token_hash = _hash_token(raw)
    expires_at = (datetime.now(timezone.utc) + timedelta(seconds=ttl)).isoformat()
    conn = _connect(db_path)
    try:
        conn.execute(
            "INSERT INTO refresh_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)",
            (token_hash, user_id, expires_at),
        )
        conn.commit()
    finally:
        conn.close()
    return raw


def verify_refresh_token(db_path: Path, raw_token: str) -> str | None:
    """Verify refresh token. Returns user_id or None."""
    token_hash = _hash_token(raw_token)
    conn = _connect(db_path)
    try:
        row = conn.execute(
            "SELECT user_id, expires_at, revoked FROM refresh_tokens WHERE token_hash = ?",
            (token_hash,),
        ).fetchone()
    finally:
        conn.close()
    if not row:
        return None
    user_id, expires_at, revoked = row
    if revoked:
        return None
    if datetime.fromisoformat(expires_at) < datetime.now(timezone.utc):
        return None
    return user_id


def rotate_refresh_token(db_path: Path, old_raw: str, *, ttl: int) -> tuple[str, str] | None:
    """Revoke old refresh token and issue new one in a single transaction.

    Returns (new_raw_token, user_id) or None if the old token is invalid/expired.
    """
    old_hash = _hash_token(old_raw)
    conn = _connect(db_path)
    try:
        row = conn.execute(
            "SELECT user_id, expires_at, revoked FROM refresh_tokens WHERE token_hash = ?",
            (old_hash,),
        ).fetchone()
        if not row:
            return None
        user_id, expires_at, revoked = row
        if revoked or datetime.fromisoformat(expires_at) < datetime.now(timezone.utc):
            return None
        # Revoke old + insert new in one transaction
        new_raw = "rf_" + secrets.token_urlsafe(32)
        new_hash = _hash_token(new_raw)
        new_expires = (datetime.now(timezone.utc) + timedelta(seconds=ttl)).isoformat()
        with conn:
            conn.execute("UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?", (old_hash,))
            conn.execute(
                "INSERT INTO refresh_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)",
                (new_hash, user_id, new_expires),
            )
        return new_raw, user_id
    finally:
        conn.close()
