"""Layer 0: API Authentication — HMAC-signed bearer tokens.

Each node gets a unique token derived from a cluster secret + node_id.
Tokens are verified without a database lookup (stateless HMAC check).

Token format: ``<node_id>.<timestamp>.<nonce>.<hmac_signature>``

Cryptographic operations (HMAC-SHA256 signing, random nonce/secret
generation) are routed through the FIPS-aware wrapper
(:mod:`greymatter_core.security.fips`), which uses the OpenSSL FIPS
provider when ``GM_FIPS_MODE=1``.  See ``docs/fips-crypto.md`` for
details.

See ``docs/auth-middleware.md`` for integration guide and security model.
"""
from __future__ import annotations

import hmac
import os
import time
import json
import sqlite3
from pathlib import Path
from typing import Optional

from greymatter_core.security.fips import (
    fips_hmac_sha256,
    fips_random_bytes,
)


# Token validity: 24 hours by default
TOKEN_TTL_SECONDS = int(os.environ.get("GM_TOKEN_TTL", "86400"))


def _get_cluster_secret() -> bytes:
    """Get or generate the cluster secret.

    The secret is stored at GM_CLUSTER_SECRET env var or
    ~/.greymatter/cluster.key (auto-generated on first use).
    """
    env_secret = os.environ.get("GM_CLUSTER_SECRET")
    if env_secret:
        return env_secret.encode()

    secret_path = Path(os.environ.get(
        "GM_CLUSTER_SECRET_PATH",
        str(Path.home() / ".greymatter" / "cluster.key"),
    ))

    if secret_path.exists():
        return secret_path.read_bytes()

    # Generate new secret
    secret = fips_random_bytes(64)
    secret_path.parent.mkdir(parents=True, exist_ok=True)
    secret_path.write_bytes(secret)
    secret_path.chmod(0o600)
    return secret


def generate_node_token(node_id: str, secret: Optional[bytes] = None) -> str:
    """Generate a signed bearer token for a node.

    Args:
        node_id: Unique identifier for the node. Must not contain dots
            (dots break the 4-part token format during verification).
        secret: Cluster secret. Uses default if not provided.

    Returns:
        Token string: <node_id>.<timestamp>.<nonce>.<hmac_signature>
    """
    if secret is None:
        secret = _get_cluster_secret()

    timestamp = str(int(time.time()))
    nonce = fips_random_bytes(8).hex()
    payload = f"{node_id}.{timestamp}.{nonce}"
    signature = fips_hmac_sha256(secret, payload.encode()).hex()
    return f"{payload}.{signature}"


def verify_node_token(
    token: str,
    secret: Optional[bytes] = None,
    max_age: Optional[int] = None,
) -> tuple[bool, str]:
    """Verify a bearer token.

    Args:
        token: Token string to verify.
        secret: Cluster secret. Uses default if not provided.
        max_age: Maximum age in seconds. Defaults to TOKEN_TTL_SECONDS.

    Returns:
        Tuple of (valid, node_id). node_id is empty string if invalid.
    """
    if secret is None:
        secret = _get_cluster_secret()

    if max_age is None:
        max_age = TOKEN_TTL_SECONDS

    parts = token.split(".")
    if len(parts) != 4:
        return (False, "")

    node_id, timestamp_str, nonce, provided_sig = parts

    # Verify HMAC signature
    payload = f"{node_id}.{timestamp_str}.{nonce}"
    expected_sig = fips_hmac_sha256(secret, payload.encode()).hex()
    if not hmac.compare_digest(provided_sig, expected_sig):
        return (False, "")

    # Check expiry
    try:
        token_time = int(timestamp_str)
    except ValueError:
        return (False, "")

    if time.time() - token_time > max_age:
        return (False, "")

    return (True, node_id)


class TokenStore:
    """Persistent token storage with rotation support.

    Stores node tokens in a SQLite table within the local database.
    Supports token rotation without downtime (accepts old + new during
    a grace period).
    """

    def __init__(self, db_path: Optional[str] = None):
        if db_path is None:
            db_path = str(Path.home() / ".greymatter" / "tokens.db")
        Path(db_path).parent.mkdir(parents=True, exist_ok=True)
        self._conn = sqlite3.connect(db_path)
        self._conn.execute("""
            CREATE TABLE IF NOT EXISTS node_tokens (
                node_id TEXT PRIMARY KEY,
                token TEXT NOT NULL,
                previous_token TEXT,
                created_at REAL NOT NULL,
                rotated_at REAL
            )
        """)
        self._conn.commit()

    def issue(self, node_id: str) -> str:
        """Issue a new token for a node, rotating any existing one."""
        token = generate_node_token(node_id)

        existing = self._conn.execute(
            "SELECT token FROM node_tokens WHERE node_id = ?",
            (node_id,),
        ).fetchone()

        if existing:
            self._conn.execute(
                "UPDATE node_tokens SET previous_token = token, "
                "token = ?, rotated_at = ? WHERE node_id = ?",
                (token, time.time(), node_id),
            )
        else:
            self._conn.execute(
                "INSERT INTO node_tokens (node_id, token, created_at) "
                "VALUES (?, ?, ?)",
                (node_id, token, time.time()),
            )
        self._conn.commit()
        return token

    def verify(self, token: str) -> tuple[bool, str]:
        """Verify a token against stored tokens.

        Accepts both current and previous (rotated) tokens during
        the grace period.
        """
        return verify_node_token(token)

    def revoke(self, node_id: str) -> None:
        """Revoke all tokens for a node."""
        self._conn.execute(
            "DELETE FROM node_tokens WHERE node_id = ?",
            (node_id,),
        )
        self._conn.commit()

    def list_nodes(self) -> list[dict]:
        """List all nodes with token status."""
        rows = self._conn.execute(
            "SELECT node_id, created_at, rotated_at FROM node_tokens"
        ).fetchall()
        return [
            {"node_id": r[0], "created_at": r[1], "rotated_at": r[2]}
            for r in rows
        ]

    def close(self):
        self._conn.close()
