# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Service specifications for self-hosted server provisioning.

Defines the catalog of services that Familiar can provision and manage,
including container images, port mappings, volume mounts, health checks,
and security profiles (capabilities, resource limits, credential keys).

Security model follows:
- NIST SP 800-190 (Application Container Security Guide)
- OWASP Top 10 for Agentic Applications (2026)
- Podman rootless hardening best practices
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional


class ServiceType(str, Enum):
    """How the service is run."""

    NATIVE = "native"  # Pure Python, in-process
    CONTAINER = "container"  # Podman / Docker container


@dataclass
class ServiceSpec:
    """Specification for a provisionable service."""

    key: str
    display_name: str
    description: str
    icon: str  # FontAwesome class (fa-*)
    service_type: ServiceType

    # Container fields
    image: str = ""
    ports: Dict[int, int] = field(default_factory=dict)  # host → container
    volumes: Dict[str, str] = field(default_factory=dict)  # host → container
    env_vars: Dict[str, str] = field(default_factory=dict)

    # Health check
    health_check_path: str = ""
    health_check_port: int = 0

    # Native service
    native_module: str = ""  # e.g. "familiar.skills.email.server"
    systemctl_unit: Optional[str] = None  # e.g. "cockpit.socket" for native systemd units

    # Startup ordering (container services only)
    depends_on: List[str] = field(default_factory=list)  # other service keys that must start first

    # Metadata
    default_container_name: str = ""
    doc_url: str = ""
    job_classes: List[str] = field(default_factory=list)
    connect_key: str = ""  # Key in connect wizard, if any
    min_ram_gb: float = 0.5

    # ── Security profile ──────────────────────────────────────────────
    # Capabilities: Linux caps the container needs beyond --cap-drop=ALL.
    # Most services need zero. Only add what's strictly required.
    capabilities: List[str] = field(default_factory=list)

    # Read-only root filesystem — prevents writes to container rootfs.
    # Services that need writable dirs use tmpfs_mounts instead.
    read_only_root: bool = True

    # Writable tmpfs mounts inside the container (for /tmp, /run, etc.)
    tmpfs_mounts: List[str] = field(default_factory=list)

    # Resource limits — prevent one container from killing the host (ASI08)
    memory_limit: str = "512m"
    cpu_limit: float = 1.0

    # Container UID — if the image runs as a non-root user (e.g. UID 2000),
    # set this so provisioning can chown volume dirs for rootless Podman.
    # 0 means root inside the container (no chown needed).
    container_uid: int = 0

    # Prevent privilege escalation inside the container
    no_new_privileges: bool = True

    # Bind ports to 127.0.0.1 only — invisible to the network by default
    localhost_only: bool = True

    # Whether this service has a browser-accessible web UI.
    # API-only services (e.g. CrowdSec LAPI) set this to False.
    web_ui: bool = True

    # Subpath for the web UI (e.g. "/admin" for Pi-hole).
    # Appended to the base URL when generating clickable links.
    web_ui_path: str = ""

    # Config files to write into volume dirs during provisioning.
    # Keys are paths relative to ~/.familiar/services/<key>/,
    # values are the file contents.
    config_files: Dict[str, str] = field(default_factory=dict)

    # Post-provision setup notes shown to the user after first deploy
    setup_notes: str = ""

    # Default credentials shipped by the upstream container image.
    # List of {"label": "Admin", "username": "admin@localhost", "password": "admin"}
    # These are NOT secrets we generate — they're public knowledge baked into the image.
    default_credentials: List[Dict[str, str]] = field(default_factory=list)

    # Env var keys that need generated secrets at provision time.
    # Values are generated via secrets.token_urlsafe(24) and stored in
    # ~/.familiar/services/secrets.json (chmod 600). Never hardcoded.
    credential_keys: List[str] = field(default_factory=list)

    def __post_init__(self):
        if not self.default_container_name and self.service_type == ServiceType.CONTAINER:
            self.default_container_name = f"familiar-{self.key}"

    @property
    def volume_base(self) -> str:
        """Base path for this service's persistent data."""
        return f"~/.familiar/services/{self.key}"

    @property
    def web_url(self) -> str:
        """Full localhost URL including web_ui_path (e.g. http://localhost:8053/admin)."""
        if not self.health_check_port:
            return ""
        base = f"http://localhost:{self.health_check_port}"
        return f"{base}{self.web_ui_path}" if self.web_ui_path else base

    def get_resolved_volumes(self, home: str) -> Dict[str, str]:
        """Resolve ~ in volume paths to actual home directory."""
        resolved = {}
        for host_path, container_path in self.volumes.items():
            resolved_host = host_path.replace("~", home)
            resolved[resolved_host] = container_path
        return resolved

    def to_dict(self) -> dict:
        """Serialize for API responses."""
        return {
            "key": self.key,
            "display_name": self.display_name,
            "description": self.description,
            "icon": self.icon,
            "service_type": self.service_type.value,
            "image": self.image,
            "ports": self.ports,
            "health_check_path": self.health_check_path,
            "health_check_port": self.health_check_port,
            "default_container_name": self.default_container_name,
            "doc_url": self.doc_url,
            "job_classes": self.job_classes,
            "connect_key": self.connect_key,
            "min_ram_gb": self.min_ram_gb,
            "depends_on": self.depends_on,
            "systemctl_unit": self.systemctl_unit,
            "localhost_only": self.localhost_only,
            "memory_limit": self.memory_limit,
            "read_only_root": self.read_only_root,
            "setup_notes": self.setup_notes,
            "default_credentials": self.default_credentials,
            "web_ui": self.web_ui,
            "web_ui_path": self.web_ui_path,
        }


# =============================================================================
# SERVICE CATALOG
# =============================================================================

SERVICE_SPECS: Dict[str, ServiceSpec] = {}


def _register(spec: ServiceSpec) -> ServiceSpec:
    SERVICE_SPECS[spec.key] = spec
    return spec


# ── Email service ────────────────────────────────────────────────────

_register(
    ServiceSpec(
        key="email_server",
        display_name="Stalwart Mail",
        description="Full-featured self-hosted email server. "
        "SMTP, IMAP, JMAP with admin UI, spam filtering, DKIM/SPF/DMARC, "
        "and automatic TLS. Replaces Gmail/Outlook for privacy-first email.",
        icon="fa-envelope",
        service_type=ServiceType.CONTAINER,
        image="stalwartlabs/mail-server:v0.11",
        ports={
            8010: 8080,  # Admin UI / JMAP (HTTP)
            2525: 25,  # SMTP inbound
            5870: 587,  # Submission (STARTTLS)
            4650: 465,  # SMTPS (implicit TLS)
            9930: 993,  # IMAPS
            1430: 143,  # IMAP
        },
        volumes={
            "~/.familiar/services/email_server/data": "/opt/stalwart",
        },
        env_vars={},
        health_check_path="/healthz/live",
        health_check_port=8010,
        doc_url="https://stalw.art/docs/",
        job_classes=[
            "helper",
            "business_buddy",
            "social_worker",
            "nonprofit_director",
            "chef",
            "artist",
        ],
        connect_key="email",
        min_ram_gb=0.5,
        # Security: mail server needs to bind standard ports inside the container
        read_only_root=False,
        memory_limit="512m",
        setup_notes=(
            "Admin credentials are auto-generated on first start — check the "
            "container logs (Server → Logs → Stalwart Mail) for the initial "
            "admin password. Open the admin UI at the URL above, change the "
            "password, then create your mailbox. Use Connect → Email to link "
            "Familiar to your new mailbox via IMAP/SMTP (localhost ports)."
        ),
        default_credentials=[
            {
                "label": "Admin",
                "username": "admin",
                "password": "(auto-generated — check container logs)",
            },
        ],
    )
)

# ── Native services ──────────────────────────────────────────────────

_register(
    ServiceSpec(
        key="rendezvous",
        display_name="Rendezvous Server",
        description="Lightweight peer registry for internet-wide mesh discovery.",
        icon="fa-satellite-dish",
        service_type=ServiceType.NATIVE,
        native_module="familiar.core.mesh.rendezvous",
        health_check_path="/api/v1/health",
        health_check_port=18790,
        job_classes=[],
        min_ram_gb=0.05,
    )
)

# ── Container services ──────────────────────────────────────────────

_register(
    ServiceSpec(
        key="gitea",
        display_name="Gitea",
        description="Lightweight self-hosted Git service (Forgejo fork). "
        "Host your own repositories, issues, and CI/CD.",
        icon="fa-code-branch",
        service_type=ServiceType.CONTAINER,
        image="codeberg.org/forgejo/forgejo:10",
        ports={3000: 3000, 2222: 22},
        volumes={
            "~/.familiar/services/gitea/data": "/data",
        },
        env_vars={
            "USER_UID": "1000",
            "USER_GID": "1000",
        },
        health_check_path="/api/v1/settings/api",
        health_check_port=3000,
        doc_url="https://forgejo.org/docs/latest/",
        job_classes=["artist", "business_buddy", "nonprofit_director"],
        connect_key="gitea",
        min_ram_gb=0.5,
        # Security: entrypoint does chown then su-exec to git user, SSH on port 22.
        # no_new_privileges must be False — su-exec needs privilege transition.
        read_only_root=False,
        capabilities=["CHOWN", "SETUID", "SETGID", "FOWNER", "DAC_OVERRIDE", "NET_BIND_SERVICE"],
        no_new_privileges=False,
        memory_limit="1g",
        setup_notes=(
            "Open Gitea at the URL above to complete initial setup. "
            "Create an admin account, then go to Settings → Applications "
            "to generate an API token for Familiar."
        ),
    )
)

_register(
    ServiceSpec(
        key="mealie",
        display_name="Mealie",
        description="Recipe manager and meal planner. "
        "Import recipes from any URL, plan meals, generate shopping lists.",
        icon="fa-utensils",
        service_type=ServiceType.CONTAINER,
        image="ghcr.io/mealie-recipes/mealie:v2.7.0",
        ports={9925: 9000},
        volumes={
            "~/.familiar/services/mealie/data": "/app/data",
        },
        env_vars={
            "ALLOW_SIGNUP": "true",
            "TZ": "UTC",
            "BASE_URL": "http://localhost:9925",
        },
        health_check_path="/api/app/about",
        health_check_port=9925,
        doc_url="https://docs.mealie.io/",
        job_classes=["chef"],
        connect_key="mealie",
        min_ram_gb=0.5,
        # Security: entrypoint does chown/usermod for dedicated user
        read_only_root=False,
        capabilities=["CHOWN", "SETUID", "SETGID", "FOWNER", "DAC_OVERRIDE"],
        tmpfs_mounts=["/tmp"],
        setup_notes=(
            "Open Mealie at the URL above to complete registration. "
            "Set Seed Data to No during setup to avoid a known upstream bug. "
            "After registering, go to User Settings → API Tokens to create "
            "a token, then use Connect → Mealie to link it to Familiar."
        ),
    )
)

_register(
    ServiceSpec(
        key="joplin",
        display_name="Joplin Server",
        description="Open-source note-taking and to-do application server. "
        "Sync notes across devices with end-to-end encryption.",
        icon="fa-sticky-note",
        service_type=ServiceType.CONTAINER,
        image="joplin/server:latest",
        container_uid=1001,
        ports={22300: 22300},
        volumes={
            "~/.familiar/services/joplin/data": "/data",
        },
        env_vars={
            "APP_PORT": "22300",
            "APP_BASE_URL": "http://localhost:22300",
            "DB_CLIENT": "sqlite3",
        },
        health_check_path="/api/ping",
        health_check_port=22300,
        doc_url="https://joplinapp.org/help/",
        job_classes=[
            "social_worker",
            "business_buddy",
            "nonprofit_director",
            "helper",
            "chef",
            "artist",
        ],
        connect_key="joplin",
        min_ram_gb=0.5,
        # Security: PM2 writes to /opt/pm2, Node writes to /home/joplin
        read_only_root=False,
        tmpfs_mounts=["/tmp"],
        setup_notes=(
            "Log in with the default credentials below, then change the password immediately. "
            "Create a user account for yourself, copy your API token from "
            "Profile → API Tokens, then use Connect → Joplin to link it to Familiar."
        ),
        default_credentials=[
            {"label": "Admin", "username": "admin@localhost", "password": "admin"},
        ],
    )
)

_register(
    ServiceSpec(
        key="pihole",
        display_name="Pi-hole",
        description="Network-wide ad blocker and DNS sinkhole. "
        "Blocks ads and trackers for all devices on your network.",
        icon="fa-shield-alt",
        service_type=ServiceType.CONTAINER,
        image="pihole/pihole:2024.07.0",
        ports={8053: 80, 5353: 53},
        volumes={
            "~/.familiar/services/pihole/etc": "/etc/pihole",
            "~/.familiar/services/pihole/dnsmasq": "/etc/dnsmasq.d",
        },
        env_vars={
            "TZ": "UTC",
        },
        health_check_path="/admin/api.php?summary",
        health_check_port=8053,
        doc_url="https://docs.pi-hole.net/",
        job_classes=[],
        min_ram_gb=0.25,
        capabilities=["CHOWN", "SETUID", "SETGID", "NET_BIND_SERVICE"],
        read_only_root=False,
        memory_limit="256m",
        web_ui_path="/admin",
        credential_keys=["WEBPASSWORD"],
        setup_notes=(
            "Admin password was auto-generated and stored in "
            "~/.familiar/services/secrets.json. Open the Pi-hole dashboard "
            "at http://localhost:8053/admin. DNS is on port 5353 — "
            "set your router or device DNS to 127.0.0.1:5353 to use it."
        ),
    )
)

_register(
    ServiceSpec(
        key="nextcloud",
        display_name="Nextcloud",
        description="Self-hosted productivity platform — files, calendar, contacts, "
        "and collaborative editing.",
        icon="fa-cloud",
        service_type=ServiceType.CONTAINER,
        image="nextcloud:29",
        ports={8080: 80},
        volumes={
            "~/.familiar/services/nextcloud/data": "/var/www/html",
        },
        env_vars={
            "NEXTCLOUD_ADMIN_USER": "admin",
        },
        health_check_path="/status.php",
        health_check_port=8080,
        doc_url="https://docs.nextcloud.com/",
        job_classes=["artist", "social_worker", "business_buddy", "nonprofit_director"],
        connect_key="nextcloud",
        min_ram_gb=1.0,
        # Security: entrypoint does rsync --chown for file ownership,
        # Apache needs setuid/setgid for workers, NET_BIND_SERVICE for port 80
        read_only_root=False,
        capabilities=[
            "CHOWN",
            "SETUID",
            "SETGID",
            "FOWNER",
            "DAC_OVERRIDE",
            "NET_BIND_SERVICE",
        ],
        memory_limit="1g",
        credential_keys=["NEXTCLOUD_ADMIN_PASSWORD"],
        setup_notes=(
            "Log in with username 'admin' and the auto-generated password shown "
            "in your credentials below. Then use Connect → Nextcloud to link your account."
        ),
        default_credentials=[
            {
                "label": "Admin",
                "username": "admin",
                "password": "(auto-generated — see credentials)",
            },
        ],
    )
)

_register(
    ServiceSpec(
        key="jellyfin",
        display_name="Jellyfin",
        description="Free media system for streaming your movies, TV shows, music, and photos.",
        icon="fa-film",
        service_type=ServiceType.CONTAINER,
        image="jellyfin/jellyfin:10.10",
        ports={8096: 8096},
        volumes={
            "~/.familiar/services/jellyfin/config": "/config",
            "~/.familiar/services/jellyfin/cache": "/cache",
        },
        env_vars={},
        health_check_path="/System/Info/Public",
        health_check_port=8096,
        doc_url="https://jellyfin.org/docs/",
        job_classes=["artist"],
        min_ram_gb=1.0,
        # Security: needs writable config/cache for transcoding
        read_only_root=False,
        memory_limit="2g",
    )
)

_register(
    ServiceSpec(
        key="searxng",
        display_name="SearXNG",
        description="Privacy-respecting metasearch engine. "
        "Aggregate results from 70+ search engines without tracking.",
        icon="fa-search",
        service_type=ServiceType.CONTAINER,
        image="searxng/searxng:latest",
        ports={8888: 8080},
        volumes={
            "~/.familiar/services/searxng/data": "/etc/searxng",
        },
        env_vars={
            "SEARXNG_BASE_URL": "http://localhost:8888/",
        },
        health_check_path="/healthz",
        health_check_port=8888,
        doc_url="https://docs.searxng.org/",
        job_classes=["helper"],
        connect_key="searxng",
        min_ram_gb=0.25,
        tmpfs_mounts=["/tmp"],
        memory_limit="256m",
    )
)

# ── Security & monitoring services ───────────────────────────────

_register(
    ServiceSpec(
        key="cockpit",
        display_name="Cockpit",
        description="Web-based system administration interface. "
        "Manage network, storage, containers, and services from your browser.",
        icon="fa-tachometer-alt",
        service_type=ServiceType.NATIVE,
        systemctl_unit="cockpit.socket",
        health_check_path="/cockpit/login",
        health_check_port=9090,
        doc_url="https://cockpit-project.org/documentation.html",
        job_classes=["network_admin"],
        min_ram_gb=0.1,
    )
)

_register(
    ServiceSpec(
        key="netdata",
        display_name="Netdata",
        description="Real-time performance and health monitoring. "
        "Thousands of metrics, zero configuration, beautiful dashboards.",
        icon="fa-chart-line",
        service_type=ServiceType.CONTAINER,
        image="netdata/netdata:v2.3",
        ports={19999: 19999},
        volumes={
            "~/.familiar/services/netdata/config": "/etc/netdata",
            "~/.familiar/services/netdata/lib": "/var/lib/netdata",
            "~/.familiar/services/netdata/cache": "/var/cache/netdata",
        },
        env_vars={
            "NETDATA_CLAIM_TOKEN": "",
            "NETDATA_CLAIM_URL": "https://app.netdata.cloud",
        },
        health_check_path="/api/v1/info",
        health_check_port=19999,
        doc_url="https://learn.netdata.cloud/docs/",
        job_classes=["network_admin"],
        min_ram_gb=0.25,
        # Security: needs SYS_PTRACE to monitor host processes
        capabilities=["SYS_PTRACE"],
        read_only_root=False,
        memory_limit="512m",
    )
)

_register(
    ServiceSpec(
        key="uptime_kuma",
        display_name="Uptime Kuma",
        description="Self-hosted monitoring tool. "
        "Track uptime of websites, APIs, and services with beautiful status pages.",
        icon="fa-heartbeat",
        service_type=ServiceType.CONTAINER,
        image="louislam/uptime-kuma:1",
        ports={3001: 3001},
        volumes={
            "~/.familiar/services/uptime_kuma/data": "/app/data",
        },
        env_vars={},
        health_check_path="/api/status-page/heartbeat/all",
        health_check_port=3001,
        doc_url="https://github.com/louislam/uptime-kuma/wiki",
        job_classes=["network_admin"],
        connect_key="uptime_kuma",
        min_ram_gb=0.25,
        # Security: entrypoint uses setpriv/setgroups for user switching
        capabilities=["CHOWN", "SETUID", "SETGID"],
        read_only_root=False,
        tmpfs_mounts=["/tmp"],
        memory_limit="256m",
    )
)

_register(
    ServiceSpec(
        key="wazuh_manager",
        display_name="Wazuh Manager",
        description="SIEM engine and security event manager. "
        "Collects, analyzes, and correlates security events from enrolled agents.",
        icon="fa-shield-alt",
        service_type=ServiceType.CONTAINER,
        image="wazuh/wazuh-manager:4.9.0",
        ports={55000: 55000, 51514: 1514, 51515: 1515},
        volumes={
            "~/.familiar/services/wazuh_manager/ossec": "/var/ossec/data",
            "~/.familiar/services/wazuh_manager/api_config": "/var/ossec/api/configuration",
            "~/.familiar/services/wazuh_manager/logs": "/var/ossec/logs",
        },
        env_vars={
            "INDEXER_URL": "http://wazuh_indexer:9200",
            "INDEXER_USERNAME": "admin",
            "FILEBEAT_SSL_VERIFICATION_MODE": "none",
        },
        health_check_path="/",
        health_check_port=55000,
        web_ui=False,  # API-only — no browser dashboard
        doc_url="https://documentation.wazuh.com/",
        job_classes=["security_analyst"],
        min_ram_gb=2.0,
        # Security: SIEM needs writable logs, higher memory for indexing
        read_only_root=False,
        memory_limit="2g",
        cpu_limit=2.0,
        credential_keys=["INDEXER_PASSWORD", "API_PASSWORD"],
    )
)

_register(
    ServiceSpec(
        key="wazuh_indexer",
        display_name="Wazuh Indexer",
        description="OpenSearch-based log indexer for Wazuh SIEM. "
        "Stores and indexes all security events for fast querying.",
        icon="fa-database",
        service_type=ServiceType.CONTAINER,
        image="wazuh/wazuh-indexer:4.9.0",
        container_uid=1000,
        ports={9200: 9200},
        volumes={
            "~/.familiar/services/wazuh_indexer/data": "/var/lib/wazuh-indexer",
            "~/.familiar/services/wazuh_indexer/opensearch.yml": "/usr/share/wazuh-indexer/opensearch.yml",
        },
        env_vars={
            "OPENSEARCH_JAVA_OPTS": "-Xms512m -Xmx512m",
        },
        config_files={
            "opensearch.yml": (
                'network.host: "0.0.0.0"\n'
                'node.name: "node-1"\n'
                "cluster.initial_master_nodes:\n"
                '- "node-1"\n'
                'cluster.name: "wazuh-cluster"\n'
                'node.max_local_storage_nodes: "3"\n'
                "path.data: /var/lib/wazuh-indexer\n"
                "path.logs: /var/log/wazuh-indexer\n"
                "\n"
                "plugins.security.disabled: true\n"
            ),
        },
        health_check_path="/_cluster/health",
        health_check_port=9200,
        web_ui=False,  # REST API — no browser dashboard
        depends_on=["wazuh_manager"],
        doc_url="https://documentation.wazuh.com/",
        job_classes=["security_analyst"],
        min_ram_gb=1.0,
        read_only_root=False,
        memory_limit="1g",
        setup_notes=(
            "Wazuh Indexer (OpenSearch) requires vm.max_map_count >= 262144. "
            "This is configured automatically during provisioning."
        ),
    )
)

_register(
    ServiceSpec(
        key="wazuh_dashboard",
        display_name="Wazuh Dashboard",
        description="Security analytics dashboard for Wazuh SIEM. "
        "Visualize alerts, vulnerabilities, compliance status, and threat timelines.",
        icon="fa-chart-bar",
        service_type=ServiceType.CONTAINER,
        image="wazuh/wazuh-dashboard:4.9.0",
        container_uid=1000,
        ports={5601: 5601},
        volumes={
            "~/.familiar/services/wazuh_dashboard/config": "/usr/share/wazuh-dashboard/config",
        },
        env_vars={
            "INDEXER_USERNAME": "admin",
            "WAZUH_API_URL": "https://wazuh_manager",
            "DASHBOARD_USERNAME": "kibanaserver",
        },
        config_files={
            "config/opensearch_dashboards.yml": (
                "server.host: 0.0.0.0\n"
                "server.port: 5601\n"
                "opensearch.hosts: http://wazuh_indexer:9200\n"
                "opensearch.ssl.verificationMode: none\n"
                "opensearch.username: admin\n"
                "opensearch.password: admin\n"
                'opensearch.requestHeadersAllowlist: ["securitytenant","Authorization"]\n'
                "opensearch_security.multitenancy.enabled: false\n"
                'opensearch_security.readonly_mode.roles: ["kibana_read_only"]\n'
                "server.ssl.enabled: false\n"
                "uiSettings.overrides.defaultRoute: /app/wz-home\n"
            ),
        },
        health_check_path="/app/wazuh",
        health_check_port=5601,
        web_ui=True,
        web_ui_path="/app/wazuh",
        depends_on=["wazuh_manager", "wazuh_indexer"],
        doc_url="https://documentation.wazuh.com/",
        job_classes=["security_analyst"],
        min_ram_gb=0.5,
        read_only_root=False,
        no_new_privileges=False,  # Node.js needs execve
        capabilities=["SETUID", "SETGID", "CHOWN"],
        memory_limit="1g",
        credential_keys=["INDEXER_PASSWORD", "DASHBOARD_PASSWORD"],
    )
)

_register(
    ServiceSpec(
        key="spiderfoot",
        display_name="SpiderFoot",
        description="Automated OSINT reconnaissance platform. "
        "Scans targets across 200+ data sources for footprinting and investigation.",
        icon="fa-spider",
        service_type=ServiceType.CONTAINER,
        image="dtagdevsec/spiderfoot:24.04.1",
        container_uid=2000,
        ports={5001: 8080},
        volumes={
            "~/.familiar/services/spiderfoot/data": "/home/spiderfoot/.spiderfoot",
        },
        env_vars={},
        health_check_path="/spiderfoot/",
        health_check_port=5001,
        web_ui_path="/spiderfoot/",
        doc_url="https://www.spiderfoot.net/documentation/",
        job_classes=["osint_researcher", "security_analyst"],
        min_ram_gb=0.5,
        read_only_root=False,
        memory_limit="512m",
    )
)

_register(
    ServiceSpec(
        key="crowdsec",
        display_name="CrowdSec",
        description="Collaborative intrusion prevention system. "
        "Detects and blocks malicious IPs using crowd-sourced threat intelligence.",
        icon="fa-shield-virus",
        service_type=ServiceType.CONTAINER,
        image="crowdsecurity/crowdsec:latest",
        ports={8180: 8080, 6060: 6060},
        volumes={
            "~/.familiar/services/crowdsec/config": "/etc/crowdsec",
            "~/.familiar/services/crowdsec/data": "/var/lib/crowdsec/data",
        },
        env_vars={
            "COLLECTIONS": "crowdsecurity/linux crowdsecurity/sshd",
        },
        health_check_path="/health",
        health_check_port=8180,
        doc_url="https://docs.crowdsec.net/",
        job_classes=["security_analyst", "network_admin"],
        min_ram_gb=0.25,
        # Security: needs NET_BIND_SERVICE for packet inspection
        capabilities=["NET_BIND_SERVICE", "NET_RAW"],
        read_only_root=False,
        memory_limit="256m",
        web_ui=False,  # API-only (LAPI) — no browser dashboard
    )
)

# ── Infrastructure services ───────────────────────────────────────

_register(
    ServiceSpec(
        key="caddy",
        display_name="Caddy",
        description="Automatic HTTPS reverse proxy. "
        "Provides TLS termination and per-service subdomains for all Familiar services.",
        icon="fa-lock",
        service_type=ServiceType.CONTAINER,
        image="caddy:2.9",
        ports={8880: 80, 8443: 443},
        volumes={
            "~/.familiar/services/caddy/data": "/data",
            "~/.familiar/services/caddy/config": "/config",
            "~/.familiar/services/caddy/Caddyfile": "/etc/caddy/Caddyfile",
        },
        env_vars={},
        health_check_path="/",
        health_check_port=8880,
        doc_url="https://caddyserver.com/docs/",
        job_classes=[],  # Infrastructure — not tied to a job class
        min_ram_gb=0.1,
        # Security: Caddy needs to bind 80/443 but is otherwise locked down
        capabilities=["NET_BIND_SERVICE"],
        tmpfs_mounts=["/tmp"],
        memory_limit="128m",
        # Caddy must be reachable from the LAN for TLS to work
        localhost_only=False,
        web_ui=False,  # Reverse proxy — no user-facing dashboard
    )
)

# ── Social / Federation services ──────────────────────────────────

_register(
    ServiceSpec(
        key="gotosocial",
        display_name="GoToSocial",
        description="Lightweight ActivityPub social media server. Mastodon-compatible federation.",
        icon="fa-hashtag",
        service_type=ServiceType.CONTAINER,
        image="superseriousbusiness/gotosocial:0.17",
        ports={8081: 8080},
        volumes={
            "~/.familiar/services/gotosocial/data": "/gotosocial/storage",
        },
        env_vars={
            "GTS_HOST": "localhost:8081",
            "GTS_DB_TYPE": "sqlite",
            "GTS_DB_ADDRESS": "/gotosocial/storage/sqlite.db",
            "GTS_LETSENCRYPT_ENABLED": "false",
            "GTS_ACCOUNTS_REGISTRATION_OPEN": "false",
        },
        health_check_path="/api/v1/instance",
        health_check_port=8081,
        doc_url="https://docs.gotosocial.org/",
        job_classes=["artist", "social_worker", "nonprofit_director", "business_buddy"],
        connect_key="mastodon",
        min_ram_gb=0.25,
        tmpfs_mounts=["/tmp"],
        memory_limit="256m",
    )
)

_register(
    ServiceSpec(
        key="bluesky_pds",
        display_name="Bluesky PDS",
        description="Personal Data Server for the AT Protocol. Self-host your Bluesky identity.",
        icon="fa-cloud-upload-alt",
        service_type=ServiceType.CONTAINER,
        image="ghcr.io/bluesky-social/pds:0.4",
        ports={2583: 3000},
        volumes={
            "~/.familiar/services/bluesky_pds/data": "/pds",
        },
        env_vars={
            "PDS_HOSTNAME": "localhost",
            "PDS_DATA_DIRECTORY": "/pds",
            "PDS_BLOBSTORE_DISK_LOCATION": "/pds/blocks",
            "PDS_DID_PLC_URL": "https://plc.directory",
            "PDS_BSKY_APP_VIEW_URL": "https://api.bsky.app",
            "PDS_BSKY_APP_VIEW_DID": "did:web:api.bsky.app",
            "PDS_REPORT_SERVICE_URL": "https://mod.bsky.app",
            "PDS_REPORT_SERVICE_DID": "did:plc:ar7c4by46qjdydhdevvrndac",
            "PDS_CRAWLERS": "https://bsky.network",
            "LOG_ENABLED": "true",
        },
        health_check_path="/xrpc/_health",
        health_check_port=2583,
        doc_url="https://github.com/bluesky-social/pds",
        job_classes=["artist", "social_worker", "nonprofit_director"],
        connect_key="bluesky",
        min_ram_gb=0.5,
        tmpfs_mounts=["/tmp"],
        memory_limit="512m",
        # Bluesky PDS already generates secrets in manager.py — keep that logic
        credential_keys=["PDS_JWT_SECRET", "PDS_ADMIN_PASSWORD"],
    )
)

_register(
    ServiceSpec(
        key="vaultwarden",
        display_name="Vaultwarden",
        description="Self-hosted Bitwarden-compatible password manager. "
        "Store and auto-fill passwords, TOTP codes, and secure notes.",
        icon="fa-key",
        service_type=ServiceType.CONTAINER,
        image="vaultwarden/server:1.32.7",
        ports={8060: 80},
        volumes={
            "~/.familiar/services/vaultwarden/data": "/data",
        },
        env_vars={
            "DOMAIN": "http://localhost:8060",
            "SIGNUPS_ALLOWED": "true",
            "ADMIN_TOKEN": "",  # Generated at provision time
            "LOG_LEVEL": "warn",
            "ROCKET_PORT": "80",
        },
        health_check_path="/alive",
        health_check_port=8060,
        doc_url="https://github.com/dani-garcia/vaultwarden/wiki",
        job_classes=[
            "helper",
            "business_buddy",
            "nonprofit_director",
            "security_analyst",
            "network_admin",
        ],
        connect_key="vaultwarden",
        min_ram_gb=0.25,
        setup_notes=(
            "Vaultwarden is running at http://localhost:8060\n"
            "Admin panel: http://localhost:8060/admin\n"
            "Create your first user account at the web interface, "
            "then install the Bitwarden browser extension and point it "
            "to http://localhost:8060."
        ),
        credential_keys=["ADMIN_TOKEN"],
        read_only_root=False,  # Rocket needs writable rootfs for socket files
        tmpfs_mounts=["/tmp"],
        memory_limit="256m",
        capabilities=["NET_BIND_SERVICE"],  # Rocket binds port 80 inside container
    )
)


# ── ComfyUI (AI Image Generation) ────────────────────────────────────

_register(
    ServiceSpec(
        key="comfyui",
        display_name="ComfyUI",
        description="AI image generation platform — runs FLUX.2, Stable Diffusion, and custom models locally",
        icon="🎨",
        service_type=ServiceType.CONTAINER,
        image="ghcr.io/ai-dock/comfyui:latest",
        ports={8188: 8188},
        volumes={"data": "/workspace"},
        env_vars={
            "WEB_ENABLE_AUTH": "false",
            "PROVISIONING_SCRIPT": "",
        },
        health_check_path="/system_stats",
        health_check_port=8188,
        web_ui=True,
        web_ui_path="/",
        doc_url="https://github.com/comfyanonymous/ComfyUI",
        job_classes=["artist"],
        connect_key="comfyui",
        min_ram_gb=4.0,
        setup_notes=(
            "ComfyUI is running at http://localhost:8188\n"
            "Download models from https://civitai.com or https://huggingface.co\n"
            "Place .safetensors files in the models/checkpoints/ folder.\n"
            "Recommended: FLUX.2 Dev or SDXL for best results."
        ),
        read_only_root=False,
        tmpfs_mounts=["/tmp"],
        memory_limit="8g",
        cpu_limit=4.0,
    )
)
