# 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

    # 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

    # 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}"

    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,
        }


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

SERVICE_SPECS: Dict[str, ServiceSpec] = {}


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


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

_register(
    ServiceSpec(
        key="email_server",
        display_name="Email Server",
        description="Built-in SMTP/IMAP email server powered by Familiar's pure-Python stack.",
        icon="fa-envelope",
        service_type=ServiceType.NATIVE,
        native_module="familiar.skills.email.server",
        health_check_path="/internal/status",
        health_check_port=0,  # In-process check
        doc_url="https://github.com/omegcrash/familiar/wiki/Email-Server",
        job_classes=["helper", "business_buddy", "social_worker", "nonprofit_director",
                     "chef", "artist"],
        connect_key="email",
        min_ram_gb=0.1,
    )
)

_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",
    )
)

_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.6.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"],
    )
)

_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",
        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"],
    )
)

_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, 53: 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,
        # Security: DNS needs NET_BIND_SERVICE for port 53
        capabilities=["NET_BIND_SERVICE"],
        read_only_root=False,
        memory_limit="256m",
        credential_keys=["WEBPASSWORD"],
    )
)

_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"],
    )
)

_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, 1514: 1514, 1515: 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": "https://localhost:9200",
            "INDEXER_USERNAME": "admin",
            "FILEBEAT_SSL_VERIFICATION_MODE": "full",
        },
        health_check_path="/",
        health_check_port=55000,
        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",
        ports={9200: 9200},
        volumes={
            "~/.familiar/services/wazuh_indexer/data": "/var/lib/wazuh-indexer",
        },
        env_vars={
            "OPENSEARCH_JAVA_OPTS": "-Xms512m -Xmx512m",
        },
        health_check_path="/_cluster/health",
        health_check_port=9200,
        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",
    )
)

_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",
        ports={5601: 5601},
        volumes={
            "~/.familiar/services/wazuh_dashboard/config": "/usr/share/wazuh-dashboard/config",
        },
        env_vars={
            "INDEXER_USERNAME": "admin",
            "WAZUH_API_URL": "https://localhost",
            "DASHBOARD_USERNAME": "kibanaserver",
        },
        health_check_path="/app/wazuh",
        health_check_port=5601,
        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,
        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",
        ports={5001: 5001},
        volumes={
            "~/.familiar/services/spiderfoot/data": "/home/spiderfoot/.spiderfoot",
        },
        env_vars={},
        health_check_path="/",
        health_check_port=5001,
        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",
    )
)

# ── 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={80: 80, 443: 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=80,
        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,
    )
)

# ── 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"],
    )
)
