"""SSH connections an agent chat can act through.

User-owned, like a `Note` and unlike a `Connection`. That is the opposite of
the rule custom tools and MCP servers follow, and the difference is the point:
those are instance configuration an administrator could grant themselves in one
click anyway, while this is somebody's own machine and somebody's own key.
"Anyone in this group may log in to my server" is a different feature with a
different blast radius.

`services/sharing.py` is deliberately not involved either. Sharing grants
reading, and a host somebody else can read is a host they can log in to.

**Nothing an agent does runs on the LLeMbas machine.** A local sandbox was
designed and dropped: every hard problem in it came from executing on the host
that holds the database and the encryption key. Over SSH, isolation is whatever
host somebody points this at -- which means the security of an agent chat is the
security of that host, and nothing here can tell a throwaway container from a
production server. The admin copy says so out loud.
"""

from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING, Any

from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship

from lembas.db.base import Base, Timestamps, UUIDPrimaryKey
from lembas.db.types import JSONDict

if TYPE_CHECKING:  # pragma: no cover - annotation only
    from lembas.db.models.user import User

# How the connection authenticates.
AUTH_KEY = "key"
AUTH_PASSWORD = "password"
AUTH_METHODS = (AUTH_KEY, AUTH_PASSWORD)


class SshProfile(UUIDPrimaryKey, Timestamps, Base):
    """One host somebody can point an agent chat at."""

    __tablename__ = "ssh_profiles"
    __table_args__ = (UniqueConstraint("owner_id", "name", name="uq_ssh_profile_name"),)

    owner_id: Mapped[str] = mapped_column(
        String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
    )
    name: Mapped[str] = mapped_column(String(120), nullable=False)

    host: Mapped[str] = mapped_column(String(255), nullable=False)
    port: Mapped[int] = mapped_column(Integer, default=22, nullable=False)
    username: Mapped[str] = mapped_column(String(120), nullable=False)

    # Whether `host` resolved to loopback the last time anybody looked. Written
    # where a network call is already happening -- saving this connection, and
    # Check -- and read on every request that asks whether this connection may
    # be used at all. A column rather than a lookup because that question is
    # asked several times per page render, and `getaddrinfo` on the request path
    # makes an agent page wait out a DNS timeout for a host nobody is talking
    # to. A literal `127.0.0.1` needs none of this and is decided from the
    # string. See services/agent/hosts.py.
    #
    # False on every row an upgrade brings in, which is correct for the literal
    # case (decided from the string anyway) and optimistic for a *name* until it
    # is next saved or checked.
    resolves_here: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

    auth: Mapped[str] = mapped_column(String(16), default=AUTH_KEY, nullable=False)
    password_encrypted: Mapped[str] = mapped_column(Text, default="")
    private_key_encrypted: Mapped[str] = mapped_column(Text, default="")
    key_passphrase_encrypted: Mapped[str] = mapped_column(Text, default="")

    # One OpenSSH known_hosts line, captured the first time this host answered
    # and shown as a fingerprint to be confirmed, then pinned. Empty means
    # "never seen". Handed to asyncssh as `known_hosts=<these bytes>` and never
    # as None, which turns host key checking off altogether.
    host_key: Mapped[str] = mapped_column(Text, default="")
    # The SHA256 fingerprint of the above, so the profile page can show what was
    # accepted without parsing the line again on every render.
    host_fingerprint: Mapped[str] = mapped_column(String(120), default="")

    # Where a chat starts by default. A chat records its own, chosen when it is
    # created and fixed thereafter; this is only the suggestion in the picker.
    default_dir: Mapped[str] = mapped_column(String(500), default="")

    connect_timeout: Mapped[int] = mapped_column(Integer, default=15, nullable=False)
    enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)

    # What the last connection attempt found, for the list. `server_banner` is
    # whatever the host said about itself -- useful for telling two containers
    # apart.
    last_checked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
    last_error: Mapped[str] = mapped_column(Text, default="")
    server_info: Mapped[dict[str, Any]] = mapped_column(JSONDict, default=dict)

    owner: Mapped[User] = relationship()

    @property
    def label(self) -> str:
        return self.name or f"{self.username}@{self.host}"

    @property
    def address(self) -> str:
        return f"{self.username}@{self.host}" + (f":{self.port}" if self.port != 22 else "")

    @property
    def verified(self) -> bool:
        """Whether this host's key has been seen and pinned."""
        return bool(self.host_key)

    def __repr__(self) -> str:
        return f"<SshProfile {self.name} {self.address}>"


class Job(Timestamps, Base):
    """A command left running on the far side after the reply that started it.

    The durable record behind `services/agent/jobs.py`, which otherwise keeps
    only an in-process registry lost on restart. A background job runs for
    minutes to hours with nobody watching -- exactly the case a restart must not
    forget -- so the row lets a startup hook re-poll the job's deterministic
    exit-file and wake the model as if nothing had happened.

    The id is `jobs`'s own short hex, not a UUIDPrimaryKey, because the same id
    names the files on the machine and is quoted back by the model.
    """

    __tablename__ = "agent_jobs"

    id: Mapped[str] = mapped_column(String(32), primary_key=True)
    chat_id: Mapped[str] = mapped_column(
        String(32), ForeignKey("chats.id", ondelete="CASCADE"), index=True, nullable=False
    )
    command: Mapped[str] = mapped_column(Text, default="")
    # running | done | killed | lost. `lost` means it stopped without an exit
    # code being recorded -- killed out of band, or the host rebooted under it.
    status: Mapped[str] = mapped_column(String(16), default="running", nullable=False)
    exit_status: Mapped[int | None] = mapped_column(Integer)
    finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))

    def __repr__(self) -> str:
        return f"<Job {self.id} {self.status}>"


__all__ = ["AUTH_KEY", "AUTH_METHODS", "AUTH_PASSWORD", "Job", "SshProfile"]
