"""Versioned, secret-free repository authorization for trusted controllers."""
from __future__ import annotations

import base64
import hashlib
import json
import os
import re
import subprocess
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping, Sequence
from urllib.parse import urlsplit

WORKFLOW="factory-kubernetes"
REPOSITORY_ID=re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$")
GITHUB_REPOSITORY=re.compile(r"^[A-Za-z0-9_.-]+$")
RESOURCE_VALUE=re.compile(r"^[A-Za-z0-9.]+(?:[KMGTE]i?|m)?$")
CREDENTIAL_HANDLE=re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
ATTEMPT_ID=re.compile(r"^[0-9a-f]{24}$")
COMMIT_ID=re.compile(r"^[0-9a-f]{40,64}$")
EXECUTION_REF=re.compile(r"^refs/heads/factory-exec/([0-9a-f]{24})$")
RESULT_REF=re.compile(r"^refs/heads/factory-result/([0-9a-f]{24})$")
SHA256=re.compile(r"^[0-9a-f]{64}$")
REGISTRY_PATH=Path(__file__).parents[3] / "repository-registry" / "repositories.v1.json"


@dataclass(frozen=True)
class Credential:
    handle: str
    profile: str


@dataclass(frozen=True)
class Repository:
    id: str
    canonical_url: str
    github_owner: str
    github_repository: str
    credential: Credential
    allowed_workflows: tuple[str, ...]
    resource_policy: Mapping[str, str]
    network_policy: Mapping[str, Any]
    result_publication: Mapping[str, Any]


@dataclass(frozen=True)
class Registry:
    version: int
    digest: str
    repositories: Mapping[str, Repository]


@dataclass(frozen=True)
class AttemptEnvelope:
    repository_id: str
    registry_version: int
    registry_digest: str
    workflow: str
    credential_handle: str
    credential_profile: str
    resource_policy: Mapping[str, str]
    network_policy: Mapping[str, Any]
    result_publication: Mapping[str, Any]
    attempt_id: str
    execution_ref: str
    result_ref: str
    input_commit: str
    invocation_sha256: str
    upload_sha256: str
    issued_at: int
    expires_at: int
    signature: str

    def payload(self) -> dict[str, Any]:
        return {
            "attempt_id": self.attempt_id,
            "credential_handle": self.credential_handle,
            "credential_profile": self.credential_profile,
            "execution_ref": self.execution_ref,
            "expires_at": self.expires_at,
            "input_commit": self.input_commit,
            "invocation_sha256": self.invocation_sha256,
            "upload_sha256": self.upload_sha256,
            "issued_at": self.issued_at,
            "network_policy": self.network_policy,
            "registry_digest": self.registry_digest,
            "registry_version": self.registry_version,
            "repository_id": self.repository_id,
            "resource_policy": self.resource_policy,
            "result_publication": self.result_publication,
            "result_ref": self.result_ref,
            "workflow": self.workflow,
        }

    def serialize(self) -> str:
        return json.dumps({**self.payload(), "signature": self.signature}, sort_keys=True, separators=(",", ":"))

    @classmethod
    def parse(cls, raw: str) -> "AttemptEnvelope":
        try:
            data = json.loads(raw)
        except json.JSONDecodeError as exc:
            raise ValueError("attempt envelope is not JSON") from exc
        required = {
            "attempt_id", "credential_handle", "credential_profile", "execution_ref", "expires_at", "input_commit",
            "invocation_sha256", "upload_sha256", "issued_at", "network_policy", "registry_digest", "registry_version", "repository_id",
            "resource_policy", "result_publication", "result_ref", "signature", "workflow",
        }
        if set(data) != required:
            raise ValueError("attempt envelope has unexpected fields")
        if not isinstance(data["signature"], str):
            raise ValueError("attempt envelope signature is invalid")
        return cls(**data)


def _canonical_json(value: Any) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()


def invocation_sha256(invocation: Sequence[str]) -> str:
    return hashlib.sha256(_canonical_json(list(invocation))).hexdigest()


def _sign_payload(payload: Mapping[str, Any], private_key: bytes) -> str:
    with tempfile.TemporaryDirectory(prefix="factory-attempt-sign-") as directory:
        key = Path(directory) / "private.pem"
        payload_path = Path(directory) / "payload.json"
        key.write_bytes(private_key)
        key.chmod(0o600)
        payload_path.write_bytes(_canonical_json(payload))
        signed = subprocess.run(
            ["openssl", "pkeyutl", "-sign", "-rawin", "-inkey", str(key), "-in", str(payload_path)],
            capture_output=True, check=False, timeout=30,
        )
    if signed.returncode or len(signed.stdout) != 64:
        raise ValueError("Factory attempt private signing key is invalid")
    return base64.urlsafe_b64encode(signed.stdout).rstrip(b"=").decode()


def _verify_signature(payload: Mapping[str, Any], signature: str, public_key: bytes) -> bool:
    try:
        raw_signature = base64.urlsafe_b64decode(signature + "=" * (-len(signature) % 4))
    except (ValueError, TypeError):
        return False
    if len(raw_signature) != 64:
        return False
    with tempfile.TemporaryDirectory(prefix="factory-attempt-verify-") as directory:
        key = Path(directory) / "public.pem"
        signed = Path(directory) / "signature"
        payload_path = Path(directory) / "payload.json"
        key.write_bytes(public_key)
        signed.write_bytes(raw_signature)
        payload_path.write_bytes(_canonical_json(payload))
        verified = subprocess.run(
            ["openssl", "pkeyutl", "-verify", "-pubin", "-rawin", "-inkey", str(key), "-sigfile", str(signed), "-in", str(payload_path)],
            capture_output=True, check=False, timeout=30,
        )
    return verified.returncode == 0


def _registry_digest(data: Mapping[str, Any]) -> str:
    return hashlib.sha256(_canonical_json(data)).hexdigest()


def _validate_secret_free(value: Any) -> None:
    forbidden = {"secret", "token", "password", "private_key", "access_key"}
    if isinstance(value, dict):
        for key, nested in value.items():
            if key.lower() in forbidden:
                raise ValueError("repository registry must not contain secret material")
            _validate_secret_free(nested)
    elif isinstance(value, list):
        for nested in value:
            _validate_secret_free(nested)


def _repository(item: Mapping[str, Any]) -> Repository:
    required = {
        "id", "canonical_url", "github", "credential", "allowed_workflows", "resource_policy",
        "network_policy", "result_publication",
    }
    if set(item) != required:
        raise ValueError("repository registry entry has unexpected fields")
    repository_id = item["id"]
    if not isinstance(repository_id, str) or not REPOSITORY_ID.fullmatch(repository_id):
        raise ValueError("repository ID is invalid")
    canonical_url = item["canonical_url"]
    parsed = urlsplit(canonical_url) if isinstance(canonical_url, str) else None
    if not parsed or canonical_url != f"https://github.com/{parsed.path.lstrip('/')}" or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.hostname != "github.com":
        raise ValueError("repository canonical URL must be credential-free GitHub HTTPS")
    github = item["github"]
    credential = item["credential"]
    if not isinstance(github, dict) or set(github) != {"owner", "repository"}:
        raise ValueError("repository GitHub identity is invalid")
    if not all(isinstance(github[key], str) and GITHUB_REPOSITORY.fullmatch(github[key]) for key in github):
        raise ValueError("repository GitHub identity is invalid")
    if parsed.path != f"/{github['owner']}/{github['repository']}.git":
        raise ValueError("repository canonical URL and GitHub identity disagree")
    if not isinstance(credential, dict) or set(credential) != {"handle", "profile"} or not isinstance(credential["handle"], str) or len(credential["handle"]) > 63 or not CREDENTIAL_HANDLE.fullmatch(credential["handle"]) or not isinstance(credential["profile"], str) or not credential["profile"]:
        raise ValueError("repository credential reference is invalid")
    workflows = item["allowed_workflows"]
    if not isinstance(workflows, list) or not workflows or len(set(workflows)) != len(workflows) or any(not isinstance(value, str) or not value for value in workflows):
        raise ValueError("repository allowed workflows are invalid")
    resource_policy = item["resource_policy"]
    if not isinstance(resource_policy, dict) or set(resource_policy) != {"cpu_request", "memory_request", "cpu_limit", "memory_limit"} or any(not isinstance(value, str) or not RESOURCE_VALUE.fullmatch(value) for value in resource_policy.values()):
        raise ValueError("repository resource policy is invalid")
    network_policy = item["network_policy"]
    if not isinstance(network_policy, dict) or set(network_policy) != {"profile", "allowed_hosts"} or not isinstance(network_policy["profile"], str) or not network_policy["profile"] or not isinstance(network_policy["allowed_hosts"], list) or not network_policy["allowed_hosts"] or any(not isinstance(host, str) or not host for host in network_policy["allowed_hosts"]):
        raise ValueError("repository network policy is invalid")
    publication = item["result_publication"]
    if not isinstance(publication, dict) or set(publication) != {"result_ref_prefix", "retain_on_failure"} or not isinstance(publication["result_ref_prefix"], str) or not publication["result_ref_prefix"] or not isinstance(publication["retain_on_failure"], bool):
        raise ValueError("repository result publication policy is invalid")
    return Repository(repository_id, canonical_url, github["owner"], github["repository"], Credential(**credential), tuple(workflows), resource_policy, network_policy, publication)


def registry_path() -> Path:
    configured = os.getenv("FACTORY_REPOSITORY_REGISTRY")
    return Path(configured) if configured else REGISTRY_PATH


def load(path: Path | None = None) -> Registry:
    path = path or registry_path()
    try:
        data = json.loads(path.read_text())
    except (OSError, json.JSONDecodeError) as exc:
        raise ValueError("repository registry is unavailable") from exc
    _validate_secret_free(data)
    if not isinstance(data, dict) or set(data) != {"version", "repositories"} or data["version"] != 1 or not isinstance(data["repositories"], list):
        raise ValueError("repository registry has unsupported schema")
    repositories = tuple(_repository(item) for item in data["repositories"])
    if not repositories or len({repository.id for repository in repositories}) != len(repositories):
        raise ValueError("repository registry IDs must be unique")
    return Registry(data["version"], _registry_digest(data), {repository.id: repository for repository in repositories})


def resolve(registry: Registry, repository_id: str, workflow: str) -> Repository:
    if not isinstance(repository_id, str) or not REPOSITORY_ID.fullmatch(repository_id):
        raise ValueError("repository ID is invalid")
    repository = registry.repositories.get(repository_id)
    if repository is None:
        raise ValueError("repository ID is not registered")
    if workflow not in repository.allowed_workflows:
        raise ValueError("workflow is not authorized for repository")
    return repository


def authorize_origin(registry: Registry, repository_id: str, workflow: str, url: str) -> Repository:
    repository = resolve(registry, repository_id, workflow)
    if url != repository.canonical_url:
        raise ValueError("origin URL does not match repository registry")
    return repository


def _validate_attempt(envelope: AttemptEnvelope) -> None:
    execution = EXECUTION_REF.fullmatch(envelope.execution_ref)
    result = RESULT_REF.fullmatch(envelope.result_ref)
    if not ATTEMPT_ID.fullmatch(envelope.attempt_id) or not execution or not result:
        raise ValueError("attempt envelope transport identity is invalid")
    if execution.group(1) != envelope.attempt_id or result.group(1) != envelope.attempt_id:
        raise ValueError("attempt envelope refs do not match its identity")
    if not COMMIT_ID.fullmatch(envelope.input_commit) or not SHA256.fullmatch(envelope.invocation_sha256) or not SHA256.fullmatch(envelope.upload_sha256):
        raise ValueError("attempt envelope content identity is invalid")
    if not isinstance(envelope.issued_at, int) or not isinstance(envelope.expires_at, int) or not 60 <= envelope.expires_at - envelope.issued_at <= 86400:
        raise ValueError("attempt envelope validity window is invalid")


def issue_envelope(registry: Registry, repository_id: str, workflow: str, signing_key: bytes, *, attempt_id: str, execution_ref: str, result_ref: str, input_commit: str, invocation: Sequence[str], upload_sha256: str, ttl_seconds: int, now: int | None = None) -> AttemptEnvelope:
    repository = resolve(registry, repository_id, workflow)
    issued_at = int(time.time()) if now is None else now
    payload = {
        "attempt_id": attempt_id,
        "credential_handle": repository.credential.handle,
        "credential_profile": repository.credential.profile,
        "execution_ref": execution_ref,
        "expires_at": issued_at + ttl_seconds,
        "input_commit": input_commit,
        "invocation_sha256": invocation_sha256(invocation),
        "upload_sha256": upload_sha256,
        "issued_at": issued_at,
        "network_policy": repository.network_policy,
        "registry_digest": registry.digest,
        "registry_version": registry.version,
        "repository_id": repository.id,
        "resource_policy": repository.resource_policy,
        "result_publication": repository.result_publication,
        "result_ref": result_ref,
        "workflow": workflow,
    }
    unsigned = AttemptEnvelope(**payload, signature="")
    _validate_attempt(unsigned)
    signature = _sign_payload(payload, signing_key)
    return AttemptEnvelope(**payload, signature=signature)


def verify_envelope(raw: str, registry: Registry, verification_key: bytes, *, now: int | None = None) -> AttemptEnvelope:
    envelope = AttemptEnvelope.parse(raw)
    if not _verify_signature(envelope.payload(), envelope.signature, verification_key):
        raise ValueError("attempt envelope integrity check failed")
    _validate_attempt(envelope)
    current = int(time.time()) if now is None else now
    if envelope.issued_at > current + 60 or current >= envelope.expires_at:
        raise ValueError("attempt envelope has expired or is not yet valid")
    if envelope.registry_version != registry.version or envelope.registry_digest != registry.digest:
        raise ValueError("attempt envelope registry drift detected")
    repository = resolve(registry, envelope.repository_id, envelope.workflow)
    if (envelope.credential_handle, envelope.credential_profile) != (repository.credential.handle, repository.credential.profile):
        raise ValueError("attempt envelope credential does not match repository")
    if dict(envelope.resource_policy) != dict(repository.resource_policy) or dict(envelope.network_policy) != dict(repository.network_policy) or dict(envelope.result_publication) != dict(repository.result_publication):
        raise ValueError("attempt envelope policy drift detected")
    return envelope
