"""Agent identity, server fingerprint, and secret storage.

Responsibilities:
  * generate a stable server fingerprint (used for de-dup / re-registration)
  * persist the backend-assigned server_id into agent.conf
  * read/write the agent token under a root-only secrets dir

SECURITY: install tokens are NEVER written here. Only the long-lived agent
token is persisted (v1 returns no refresh token). The token is stored as a
plain file with restrictive permissions (0600) inside the root-only secrets
directory (0700). The agent has no third-party dependencies, so it installs and
runs on a clean Linux server without pip; sealing/encryption is a production
TODO that must not reintroduce a runtime dependency.
"""

from __future__ import annotations

import hmac
import os
import hashlib
import socket
import uuid
from pathlib import Path

import state as state_module
from config import Config
from logger import get_logger

log = get_logger("identity")


def _derive_key(fingerprint: str, salt: bytes) -> bytes:
    """Derive 256-bit key from fingerprint and salt using PBKDF2-HMAC-SHA256."""
    return hashlib.pbkdf2_hmac("sha256", fingerprint.encode("utf-8"), salt, 10000, 32)


def encrypt_token(token: str, fingerprint: str) -> str:
    """Encrypt token using standard HMAC-CTR stream cipher bound to machine fingerprint."""
    salt = os.urandom(16)
    iv = os.urandom(16)
    key = _derive_key(fingerprint, salt)
    
    plaintext = token.encode("utf-8")
    ciphertext = bytearray(len(plaintext))
    
    block_index = 0
    for offset in range(0, len(plaintext), 32):
        counter_bytes = block_index.to_bytes(4, "big")
        keystream_block = hmac.new(key, iv + counter_bytes, hashlib.sha256).digest()
        
        chunk_size = min(32, len(plaintext) - offset)
        for i in range(chunk_size):
            ciphertext[offset + i] = plaintext[offset + i] ^ keystream_block[i]
        block_index += 1
        
    payload = salt + iv + bytes(ciphertext)
    return f"wp_enc:{payload.hex()}"


def decrypt_token(encrypted_data: str, fingerprint: str) -> str:
    """Decrypt token using standard HMAC-CTR stream cipher (returns plaintext if not encrypted)."""
    if not encrypted_data.startswith("wp_enc:"):
        return encrypted_data
        
    payload = bytes.fromhex(encrypted_data[7:])
    if len(payload) < 32:
        raise ValueError("Invalid encrypted token payload length")
        
    salt = payload[:16]
    iv = payload[16:32]
    ciphertext = payload[32:]
    
    key = _derive_key(fingerprint, salt)
    plaintext = bytearray(len(ciphertext))
    
    block_index = 0
    for offset in range(0, len(ciphertext), 32):
        counter_bytes = block_index.to_bytes(4, "big")
        keystream_block = hmac.new(key, iv + counter_bytes, hashlib.sha256).digest()
        
        chunk_size = min(32, len(ciphertext) - offset)
        for i in range(chunk_size):
            plaintext[offset + i] = ciphertext[offset + i] ^ keystream_block[i]
        block_index += 1
        
    return bytes(plaintext).decode("utf-8")


def _read_first_line(path: str) -> str | None:
    try:
        with open(path, "r", encoding="utf-8") as handle:
            return handle.readline().strip() or None
    except OSError:
        return None


def _primary_mac() -> str:
    """Best-effort stable MAC-derived value from uuid.getnode()."""
    node = uuid.getnode()
    return f"{node:012x}"


def generate_fingerprint() -> str:
    """Build a stable, non-secret server fingerprint.

    Combines machine-id, hostname and a MAC-derived value, then hashes them so
    the result is opaque and contains no directly identifying data. The same
    server should produce the same fingerprint across reinstalls, which lets the
    control plane detect re-registration vs. a brand new server.
    """
    parts = [
        _read_first_line("/etc/machine-id")
        or _read_first_line("/var/lib/dbus/machine-id")
        or "",
        socket.gethostname(),
        _primary_mac(),
    ]
    digest = hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()
    return f"fp_{digest[:32]}"


def ensure_dirs(config: Config) -> None:
    """Create the runtime directory tree with appropriate permissions."""
    for directory in (
        config.etc_dir,
        config.opt_dir,
        config.var_dir,
        config.log_dir,
        config.spool_dir,
        config.jobs_dir,
        config.discovery_dir,
        config.cache_dir,
    ):
        directory.mkdir(parents=True, exist_ok=True)

    # Restrict base directories permissions for least privilege
    _chmod(config.etc_dir, 0o700)
    _chmod(config.var_dir, 0o700)
    _chmod(config.log_dir, 0o700)

    # Secrets directory is root-only (0700). chmod is a no-op on Windows dev.
    config.secrets_dir.mkdir(parents=True, exist_ok=True)
    _chmod(config.secrets_dir, 0o700)


def _chmod(path: Path, mode: int) -> None:
    try:
        path.chmod(mode)
    except (OSError, NotImplementedError):  # pragma: no cover - Windows/dev
        pass


def write_secret(config: Config, path: Path, value: str) -> None:
    """Write a secret as an encrypted file with 0600 permissions in a 0700 dir.

    The secret is encrypted bound to the machine fingerprint.
    """
    path.parent.mkdir(parents=True, exist_ok=True)
    _chmod(path.parent, 0o700)
    
    fingerprint = generate_fingerprint()
    encrypted_value = encrypt_token(value, fingerprint)
    
    path.write_text(encrypted_value, encoding="utf-8")
    _chmod(path, 0o600)
    log.info("stored secret securely at %s", path)


def read_secret(config: Config, path: Path) -> str | None:
    try:
        data = path.read_text(encoding="utf-8").strip()
        if not data:
            return None
        fingerprint = generate_fingerprint()
        return decrypt_token(data, fingerprint)
    except OSError:
        return None


def set_conf_value(config: Config, key: str, value: str) -> None:
    """Insert or update a KEY=VALUE line in agent.conf (idempotent)."""
    config.conf_file.parent.mkdir(parents=True, exist_ok=True)
    lines: list[str] = []
    if config.conf_file.is_file():
        lines = config.conf_file.read_text(encoding="utf-8").splitlines()

    replaced = False
    for index, line in enumerate(lines):
        if line.strip().startswith(f"{key}="):
            lines[index] = f"{key}={value}"
            replaced = True
            break
    if not replaced:
        lines.append(f"{key}={value}")

    config.conf_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
    _chmod(config.conf_file, 0o600)


def save_status(config: Config, status: str) -> None:
    """Persist agent status in runtime state (never in agent.conf)."""
    state_module.update_state(
        config.state_file, status=status, revoked=(status == "revoked")
    )
    config.status = status


def save_last_error(config: Config, error: str | None) -> None:
    """Persist the latest heartbeat/registration error (single-line) or clear it."""
    value = error or ""
    state_module.update_state(config.state_file, last_error=value)
    config.last_error = value


def set_registration_pending(config: Config, pending: bool, error: str | None = None) -> None:
    """Record that registration is still owed (e.g. installer could not reach API)."""
    changes: dict[str, object] = {"registration_pending": pending}
    if pending:
        changes["status"] = "pending_install"
    if error is not None:
        changes["last_error"] = error
    state_module.update_state(config.state_file, **changes)
    config.registration_pending = pending
    if "status" in changes:
        config.status = str(changes["status"])
    if error is not None:
        config.last_error = error


def store_credentials(config: Config, server_id: str, agent_token: str) -> None:
    """Persist registration results: server_id to conf, agent token to secrets.

    v1 contract returns only {agent_token, server_id, status} — there is no
    agent_id or refresh_token to store.
    """
    set_conf_value(config, "WOLFPANEL_SERVER_ID", server_id)
    write_secret(config, config.agent_token_file, agent_token)
    state_module.update_state(
        config.state_file,
        status="registered",
        last_error="",
        registration_pending=False,
        revoked=False,
    )
    config.status = "registered"
    config.last_error = ""
    config.registration_pending = False
    log.info("credentials stored for server_id=%s", server_id)


def load_agent_token(config: Config) -> str | None:
    return read_secret(config, config.agent_token_file)
