"""Frontend-neutral SSH-key protocol models.

These models describe keys discovered or generated by the daemon. The frontend
receives opaque ``KeyId`` values and semantic ``KeyStoreScope`` values; it never
supplies or derives key-directory paths. The path fields on :class:`KeySummary`
are temporary compatibility metadata for the M7 ``ssh-copy-id`` subprocess
adapter: they are not authority, and private-key contents never cross the API.
"""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from enum import Enum
from typing import NewType, Tuple

from .common import SessionId, require_identifier

KeyId = NewType("KeyId", str)

_ALLOWED_KEY_TYPES = ("ed25519", "rsa")
_MIN_RSA_BITS = 1024

# Recognizable PEM/OpenSSH private-key headers. Public-key text must never
# contain any of these markers (private material must not cross the API).
_PRIVATE_KEY_MARKERS = (
    "-----BEGIN OPENSSH PRIVATE KEY-----",
    "-----BEGIN RSA PRIVATE KEY-----",
    "-----BEGIN EC PRIVATE KEY-----",
    "-----BEGIN PRIVATE KEY-----",
)


def contains_private_key_material(text: str) -> bool:
    """True when *text* contains a recognizable private-key header.

    Single source of truth shared by :class:`PublicKeyResult` validation and
    the daemon's pre-construction public-key read check, so the two sites can
    never drift apart.
    """
    return any(marker in text for marker in _PRIVATE_KEY_MARKERS)


class KeyStoreScope(str, Enum):
    """Semantic key-store location; never a filesystem path."""

    DEFAULT = "default"
    ISOLATED = "isolated"


@dataclass(frozen=True)
class KeySummary:
    """Metadata for a daemon-discovered or generated key (never private material)."""

    key_id: KeyId
    name: str
    private_path: str = field(repr=False)
    public_path: str = field(repr=False)
    public_key_available: bool

    def __post_init__(self) -> None:
        require_identifier(self.key_id, "key id")
        for field_name in ("name", "private_path", "public_path"):
            value = getattr(self, field_name)
            if type(value) is not str:
                raise TypeError(f"{field_name} must be a string")
            if "\x00" in value:
                raise ValueError(f"{field_name} must not contain NUL")
        if type(self.public_key_available) is not bool:
            raise TypeError("public_key_available must be a boolean")
        if not self.name:
            raise ValueError("key name must be a non-empty string")
        if not os.path.isabs(self.private_path):
            raise ValueError("key private path must be absolute")
        if not os.path.isabs(self.public_path):
            raise ValueError("key public path must be absolute")
        if os.path.basename(self.private_path) != self.name:
            raise ValueError("key name must match the private path basename")
        if self.public_path != self.private_path + ".pub":
            raise ValueError("key public path must be the private path plus '.pub'")


@dataclass(frozen=True)
class ListKeysRequest:
    """List keys from one semantic key-store scope."""

    scope: KeyStoreScope = KeyStoreScope.DEFAULT

    def __post_init__(self) -> None:
        if type(self.scope) is not KeyStoreScope:
            raise TypeError("key store scope must be a KeyStoreScope")


@dataclass(frozen=True)
class DeleteKeyRequest:
    """Request deletion of one daemon-known key by opaque identity."""

    key_id: KeyId
    scope: KeyStoreScope = KeyStoreScope.DEFAULT

    def __post_init__(self) -> None:
        require_identifier(self.key_id, "key id")
        if type(self.scope) is not KeyStoreScope:
            raise TypeError("key store scope must be a KeyStoreScope")


@dataclass(frozen=True)
class DeleteKeyResult:
    """Stable result for daemon-owned key-pair deletion."""

    key_id: KeyId
    deleted: bool = True

    def __post_init__(self) -> None:
        require_identifier(self.key_id, "key id")
        if type(self.deleted) is not bool:
            raise TypeError("deleted must be a boolean")


@dataclass(frozen=True)
class KeyList:
    """Ordered key metadata from the daemon (duplicate ids rejected)."""

    keys: Tuple[KeySummary, ...]

    def __post_init__(self) -> None:
        if type(self.keys) is not tuple:
            raise TypeError("key list must be a tuple")
        for key in self.keys:
            if type(key) is not KeySummary:
                raise TypeError("key list entries must be KeySummary")
        ids = [key.key_id for key in self.keys]
        if len(set(ids)) != len(ids):
            raise ValueError("key list must not contain duplicate key ids")


@dataclass(frozen=True)
class ReadPublicKeyRequest:
    """Read public-key text for one opaque key id."""

    key_id: KeyId
    scope: KeyStoreScope = KeyStoreScope.DEFAULT

    def __post_init__(self) -> None:
        require_identifier(self.key_id, "key id")
        if type(self.scope) is not KeyStoreScope:
            raise TypeError("key store scope must be a KeyStoreScope")


@dataclass(frozen=True)
class PublicKeyResult:
    """Public-key text for one key (never private-key material)."""

    key_id: KeyId
    text: str = field(repr=False)

    def __post_init__(self) -> None:
        require_identifier(self.key_id, "key id")
        if type(self.text) is not str:
            raise TypeError("public key text must be a string")
        if "\x00" in self.text:
            raise ValueError("public key text must not contain NUL")
        if not self.text.strip():
            raise ValueError("public key text must be a non-empty string")
        if contains_private_key_material(self.text):
            raise ValueError(
                "public key text must not contain private-key material"
            )


@dataclass(frozen=True)
class GenerateKeyRequest:
    """Request a daemon-owned ``ssh-keygen`` invocation."""

    name: str
    key_type: str = "ed25519"
    key_size: int = 0
    comment: str = ""
    encrypted: bool = False
    interaction_scope_id: SessionId | None = None
    scope: KeyStoreScope = KeyStoreScope.DEFAULT

    def __post_init__(self) -> None:
        if type(self.name) is not str:
            raise TypeError("key name must be a string")
        if "\x00" in self.name:
            raise ValueError("key name must not contain NUL")
        if not self.name.strip():
            raise ValueError("key name must be a non-empty string")
        if (
            "/" in self.name
            or "\\" in self.name
            or self.name in (".", "..")
            or self.name.startswith(".")
        ):
            raise ValueError(
                "key name must not contain path separators or start with '.'"
            )
        if type(self.key_type) is not str:
            raise TypeError("key type must be a string")
        if self.key_type not in _ALLOWED_KEY_TYPES:
            raise ValueError("key type must be 'ed25519' or 'rsa'")
        if type(self.key_size) is not int:
            raise TypeError("key size must be an integer")
        if self.key_type == "ed25519" and self.key_size != 0:
            raise ValueError("ed25519 keys do not accept a key size")
        if self.key_type == "rsa" and self.key_size < _MIN_RSA_BITS:
            raise ValueError("rsa key size must be at least 1024 bits")
        if type(self.comment) is not str:
            raise TypeError("comment must be a string")
        if "\x00" in self.comment:
            raise ValueError("comment must not contain NUL")
        if type(self.encrypted) is not bool:
            raise TypeError("encrypted must be a boolean")
        if self.interaction_scope_id is not None:
            require_identifier(
                self.interaction_scope_id,
                "key interaction scope id",
            )
            if not str(self.interaction_scope_id).startswith("key-operation-"):
                raise ValueError(
                    "key interaction scope id must start with 'key-operation-'"
                )
        if self.encrypted != (self.interaction_scope_id is not None):
            raise ValueError(
                "encrypted key generation requires exactly one interaction scope"
            )
        if type(self.scope) is not KeyStoreScope:
            raise TypeError("key store scope must be a KeyStoreScope")


@dataclass(frozen=True)
class GenerateKeyResult:
    """Result of a daemon key generation."""

    key: KeySummary

    def __post_init__(self) -> None:
        if type(self.key) is not KeySummary:
            raise TypeError("generate key result requires a KeySummary")


@dataclass(frozen=True)
class VerifyKeyPassphraseRequest:
    """Request daemon-owned verification using a protected interaction secret."""

    key_path: str = field(repr=False)
    interaction_scope_id: SessionId

    def __post_init__(self) -> None:
        if type(self.key_path) is not str:
            raise TypeError("key path must be a string")
        if not self.key_path or "\x00" in self.key_path:
            raise ValueError("key path must be a non-empty NUL-free string")
        require_identifier(
            self.interaction_scope_id,
            "key interaction scope id",
        )
        if not str(self.interaction_scope_id).startswith("key-operation-"):
            raise ValueError(
                "key interaction scope id must start with 'key-operation-'"
            )


@dataclass(frozen=True)
class VerifyKeyPassphraseResult:
    """Whether the protected passphrase unlocked the selected private key."""

    valid: bool

    def __post_init__(self) -> None:
        if type(self.valid) is not bool:
            raise TypeError("key passphrase verification result must be a boolean")
