# 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

"""Constants and module-level helper functions for the connect wizard."""

from __future__ import annotations

from pathlib import Path

try:
    import fcntl
except ImportError:
    fcntl = None  # type: ignore[assignment]  # Windows / Android

import yaml

from ..formatting import FormatMode, fmt_bold, fmt_code, fmt_link

# ── Tab / Card UI constants ───────────────────────────────────────────

TAB_ORDER = ("llm", "integrations", "selfhosted", "security")

TAB_CATEGORIES: dict[str, dict] = {
    "llm": {
        "label": "LLM",
        "icon": "\u2601\ufe0f",
        "services": ["anthropic", "openai", "gemini", "ollama"],
    },
    "integrations": {
        "label": "Integrations",
        "icon": "\U0001f517",
        "services": [
            "google",
            "calendar",
            "email",
            "sms",
            "browser",
            "voice",
            "websearch",
            "healthcare",
            "github",
            "slack",
        ],
    },
    "selfhosted": {
        "label": "Self-Hosted",
        "icon": "\U0001f3e0",
        "services": [
            "jellyfin",
            "gitea",
            "homeassistant",
            "joplin",
            "pihole",
            "nextcloud",
            "proton",
            "email_server",
        ],
    },
    "security": {
        "label": "Security",
        "icon": "\U0001f512",
        "services": [
            "vaultwarden",
            "encryption",
            "phi_detection",
            "rbac",
            "user_management",
        ],
    },
}

SERVICE_DISPLAY: dict[str, str] = {
    "anthropic": "Anthropic (Claude)",
    "openai": "OpenAI (GPT)",
    "gemini": "Gemini (Google AI)",
    "ollama": "Ollama (local, free)",
    "google": "Google Workspace",
    "calendar": "Calendar (Google/CalDAV)",
    "email": "Email (IMAP/SMTP)",
    "sms": "SMS (Twilio)",
    "browser": "Browser (Playwright)",
    "voice": "Voice (Whisper)",
    "websearch": "WebSearch (DuckDuckGo)",
    "healthcare": "Healthcare / Compliance",
    "jellyfin": "Jellyfin (media)",
    "gitea": "Forgejo / Gitea (code)",
    "homeassistant": "Home Assistant",
    "joplin": "Joplin (notes)",
    "pihole": "Pi-hole / AdGuard",
    "nextcloud": "Nextcloud",
    "proton": "Proton (Bridge & VPN)",
    "email_server": "Email Server",
    "vaultwarden": "Vaultwarden (passwords)",
    "encryption": "Encryption (at rest)",
    "phi_detection": "PHI Detection (safe mode)",
    "rbac": "RBAC (role-based access)",
    "user_management": "User Management",
    "github": "GitHub (issues & PRs)",
    "slack": "Slack (Socket Mode)",
}

SERVICE_BUTTON_LABELS: dict[str, tuple[str, str]] = {
    # key -> (callback_key, button_label)
    "anthropic": ("anthropic", "Claude \u2601\ufe0f"),
    "openai": ("openai", "GPT \u2601\ufe0f"),
    "gemini": ("gemini", "Gemini \u2601\ufe0f"),
    "ollama": ("ollama", "Ollama \U0001f3e0"),
    "google": ("google_menu", "\U0001f4c5 Google"),
    "calendar": ("calendar_menu", "\U0001f4c5 Calendar"),
    "email": ("email_menu", "\U0001f4e7 Email"),
    "sms": ("sms_menu", "\U0001f4f1 SMS"),
    "browser": ("browser_menu", "\U0001f310 Browser"),
    "voice": ("voice_menu", "\U0001f3a4 Voice"),
    "websearch": ("websearch_menu", "\U0001f50d WebSearch"),
    "healthcare": ("healthcare_menu", "\U0001f3e5 Healthcare"),
    "jellyfin": ("jellyfin_menu", "\U0001f3ac Jellyfin"),
    "gitea": ("gitea_menu", "\U0001f419 Forgejo / Gitea"),
    "homeassistant": ("homeassistant_menu", "\U0001f3e0 Home Assistant"),
    "joplin": ("joplin_menu", "\U0001f4dd Joplin"),
    "pihole": ("pihole_menu", "\U0001f6e1\ufe0f Pi-hole"),
    "nextcloud": ("nextcloud_menu", "\u2601\ufe0f Nextcloud"),
    "proton": ("proton_services_menu", "\U0001f512 Proton"),
    "email_server": ("email_server_menu", "\U0001f4ec Email Server"),
    "vaultwarden": ("vaultwarden_menu", "\U0001f511 Vaultwarden"),
    "encryption": ("encryption_menu", "\U0001f510 Encryption"),
    "phi_detection": ("phi_detection_menu", "\U0001f3e5 PHI Detection"),
    "rbac": ("rbac_menu", "\U0001f6e1\ufe0f RBAC"),
    "user_management": ("user_management_menu", "\U0001f465 Users"),
    "github": ("github_menu", "\U0001f4bb GitHub"),
    "slack": ("slack_menu", "\U0001f4ac Slack"),
}

# ── Service documentation links ──────────────────────────────────

SERVICE_DOCS: dict[str, tuple[str, str]] = {
    # LLM providers
    "anthropic": (
        "https://console.anthropic.com/settings/keys",
        "Get your API key from the Anthropic Console",
    ),
    "openai": (
        "https://platform.openai.com/api-keys",
        "Generate an API key in the OpenAI dashboard",
    ),
    "gemini": (
        "https://aistudio.google.com/apikey",
        "Create an API key in Google AI Studio",
    ),
    "ollama": (
        "https://ollama.com/download",
        "Download and install Ollama for local models",
    ),
    # Self-Hosted
    "jellyfin": (
        "https://jellyfin.org/docs/general/administration/configuration/",
        "Jellyfin admin docs \u2014 find your API key under Dashboard \u2192 API Keys",
    ),
    "gitea": (
        "https://forgejo.org/docs/latest/user/api-usage/",
        "Forgejo API docs \u2014 generate a token under Settings \u2192 Applications",
    ),
    "gitea_alt": (
        "https://about.gitea.com/resources/tutorials/api-key",
        "Gitea API docs \u2014 generate a token under Settings \u2192 Applications",
    ),
    "homeassistant": (
        "https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token",
        "HA docs \u2014 create a Long-Lived Access Token in your Profile",
    ),
    "joplin": (
        "https://joplinapp.org/help/apps/clipper/",
        "Joplin Web Clipper docs \u2014 find your token in Tools \u2192 Options \u2192 Web Clipper",
    ),
    "pihole": (
        "https://docs.pi-hole.net/guides/misc/whitelist-blacklist/",
        "Pi-hole docs \u2014 find your API token under Settings \u2192 API",
    ),
    "nextcloud": (
        "https://docs.nextcloud.com/server/latest/user_manual/en/files/access_webdav.html",
        "Nextcloud WebDAV docs \u2014 use an App Password from Security settings",
    ),
    "email_server": (
        "https://docs.mailcow.email/",
        "Mailcow/email server setup guide",
    ),
    # Security
    "vaultwarden": (
        "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page",
        "Vaultwarden wiki \u2014 enable admin page to get API token",
    ),
    # Integrations
    "sms": (
        "https://www.twilio.com/docs/sms/quickstart/python",
        "Twilio SMS quickstart \u2014 get Account SID, Auth Token, and a phone number",
    ),
    "browser": (
        "https://playwright.dev/python/docs/intro",
        "Playwright Python docs \u2014 installation and browser setup",
    ),
    "voice": (
        "https://github.com/openai/whisper",
        "OpenAI Whisper GitHub \u2014 speech recognition models",
    ),
    "websearch": (
        "https://docs.searxng.org/admin/installation.html",
        "SearXNG install docs (optional \u2014 DuckDuckGo works out of the box)",
    ),
    "github": (
        "https://github.com/settings/tokens",
        "Create a PAT with repo + notifications scopes",
    ),
    "slack": (
        "https://api.slack.com/apps",
        "Create a Slack App with Socket Mode enabled",
    ),
    # Email/Calendar
    "google": (
        "https://console.cloud.google.com/apis/credentials",
        "Google Cloud Console \u2014 create OAuth 2.0 credentials (Desktop app type)",
    ),
    "caldav": (
        "https://www.calconnect.org/resources/caldav",
        "CalDAV protocol info \u2014 check your provider's docs for the server URL",
    ),
}


def _service_doc_blurb(service_key: str, mode: FormatMode) -> str:
    """Return a one-line doc link for a service, or empty string if none."""
    entry = SERVICE_DOCS.get(service_key)
    if not entry:
        return ""
    url, desc = entry
    link = fmt_link("Setup guide", url, mode)
    lines = f"\n{link} \u2014 {desc}"
    # Show alternate docs link if one exists (e.g. Forgejo + Gitea)
    alt_entry = SERVICE_DOCS.get(f"{service_key}_alt")
    if alt_entry:
        alt_url, alt_desc = alt_entry
        alt_link = fmt_link("Alt docs", alt_url, mode)
        lines += f"\n{alt_link} \u2014 {alt_desc}"
    return lines + "\n"


# Optional: caldav library for CalDAV testing
try:
    import caldav
except ImportError:
    caldav = None  # type: ignore[assignment]

# ── Email provider presets (format-neutral) ─────────────────────────

EMAIL_PRESETS: dict[str, dict] = {
    "gmail": {
        "name": "Gmail",
        "imap_server": "imap.gmail.com",
        "smtp_server": "smtp.gmail.com",
        "imap_port": 993,
        "smtp_port": 587,
        "password_help_plain": (
            "Gmail requires an App Password (not your regular password):\n\n"
            "1. Go to https://myaccount.google.com/apppasswords\n"
            '2. Select "Mail" -> "Other (Familiar)"\n'
            "3. Copy the 16-character password\n"
        ),
    },
    "outlook": {
        "name": "Outlook / Office 365",
        "imap_server": "outlook.office365.com",
        "smtp_server": "smtp.office365.com",
        "imap_port": 993,
        "smtp_port": 587,
        "password_help_plain": (
            "Use your Outlook password or an App Password if MFA is enabled.\n"
        ),
    },
    "yahoo": {
        "name": "Yahoo Mail",
        "imap_server": "imap.mail.yahoo.com",
        "smtp_server": "smtp.mail.yahoo.com",
        "imap_port": 993,
        "smtp_port": 587,
        "password_help_plain": (
            "Generate an App Password at https://login.yahoo.com/account/security\n"
        ),
    },
    "proton": {
        "name": "ProtonMail (Bridge)",
        "imap_server": "127.0.0.1",
        "smtp_server": "127.0.0.1",
        "imap_port": 1143,
        "smtp_port": 1025,
        "password_help_plain": (
            "Proton Mail Bridge runs locally and exposes IMAP/SMTP.\n\n"
            "Setup:\n"
            "1. Install Bridge from https://proton.me/mail/bridge\n"
            "   Linux: flatpak install flathub ch.protonmail.protonmail-bridge\n"
            "   Or download the .deb/.rpm from the link above\n"
            "2. Open Bridge and sign in with your Proton account\n"
            "3. Click your account name -> Mailbox configuration\n"
            "4. Copy the Bridge password (NOT your Proton password)\n"
            "   It looks like: aBcDeFgHiJkLmNoP\n"
            "5. Note your Proton email address\n\n"
            "Bridge must be running whenever Familiar checks email.\n"
            "Ports: IMAP 1143, SMTP 1025 (localhost only, no TLS needed).\n"
        ),
    },
}


def _password_help(provider_key: str, mode: FormatMode) -> str:
    """Render password help text with appropriate formatting."""
    preset = EMAIL_PRESETS[provider_key]
    plain = preset["password_help_plain"]
    if mode is FormatMode.HTML:
        # Re-render with HTML tags for richer presentation
        if provider_key == "gmail":
            return (
                f"Gmail requires an {fmt_bold('App Password', mode)} (not your regular password):\n\n"
                f"1. Go to https://myaccount.google.com/apppasswords\n"
                f'2. Select "Mail" \u2192 "Other (Familiar)"\n'
                f"3. Copy the 16-character password\n"
            )
        if provider_key == "proton":
            return (
                f"{fmt_bold('Proton Mail Bridge', mode)} runs locally and exposes IMAP/SMTP.\n\n"
                f"{fmt_bold('Setup:', mode)}\n"
                f"1. Install Bridge from https://proton.me/mail/bridge\n"
                f"   \u2022 Linux: {fmt_code('flatpak install flathub ch.protonmail.protonmail-bridge', mode)}\n"
                f"   \u2022 Or download the .deb/.rpm from the link above\n"
                f"2. Open Bridge and sign in with your Proton account\n"
                f"3. Click your account name \u2192 {fmt_bold('Mailbox configuration', mode)}\n"
                f"4. Copy the {fmt_bold('Bridge password', mode)} (NOT your Proton password)\n"
                f"   \u2022 It looks like: {fmt_code('aBcDeFgHiJkLmNoP', mode)}\n"
                f"5. Note your Proton email address\n\n"
                f"{fmt_bold('Bridge must be running', mode)} whenever Familiar checks email.\n"
                f"Ports: IMAP 1143, SMTP 1025 (localhost only, no TLS needed).\n"
            )
    return plain


# ── Env file + IMAP helpers (static, no channel dependency) ─────────


def _update_env_file(env_path, updates: dict):
    """Update or append key=value pairs in a .env file (with file locking)."""
    import shutil
    from datetime import datetime

    env_path = Path(env_path)
    env_path.parent.mkdir(parents=True, exist_ok=True)

    if not env_path.exists():
        env_path.touch()

    # Back up existing .env before any modifications
    if env_path.stat().st_size > 0:
        backup_dir = env_path.parent / "backups"
        backup_dir.mkdir(parents=True, exist_ok=True)
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_path = backup_dir / f".env.{timestamp}"
        shutil.copy2(env_path, backup_path)
        try:
            backup_path.chmod(0o600)
        except OSError:
            pass

    with open(env_path, "r+") as f:
        if fcntl is not None:
            fcntl.flock(f, fcntl.LOCK_EX)
        try:
            existing_lines = f.read().splitlines()

            updated_keys: set[str] = set()
            new_lines: list[str] = []

            for line in existing_lines:
                stripped = line.strip()
                if stripped and not stripped.startswith("#") and "=" in stripped:
                    key = stripped.split("=", 1)[0].strip()
                    if key in updates:
                        new_lines.append(f"{key}={updates[key]}")
                        updated_keys.add(key)
                        continue
                new_lines.append(line)

            for key, value in updates.items():
                if key not in updated_keys:
                    new_lines.append(f"{key}={value}")

            content = "\n".join(new_lines) + "\n"
            f.seek(0)
            f.write(content)
            f.truncate()
        finally:
            if fcntl is not None:
                fcntl.flock(f, fcntl.LOCK_UN)
    env_path.chmod(0o600)


def _update_yaml_config(updates: dict[str, dict]) -> None:
    """Merge nested key updates into ~/.familiar/config.yaml.

    *updates* maps top-level section names to dicts of key/value pairs, e.g.
    ``{"compliance": {"mode": "hipaa"}, "agent": {"enable_pii_detection": True}}``.
    """
    config_path = Path.home() / ".familiar" / "config.yaml"
    config_path.parent.mkdir(parents=True, exist_ok=True)

    data: dict = {}
    if config_path.exists():
        data = yaml.safe_load(config_path.read_text()) or {}

    for section, kvs in updates.items():
        if section not in data:
            data[section] = {}
        data[section].update(kvs)

    config_path.write_text(yaml.dump(data, default_flow_style=False))


def _test_imap_connection(env_vars: dict) -> tuple[bool, str]:
    """Test IMAP connection.  Returns (success, message)."""
    import imaplib

    try:
        server = env_vars["EMAIL_IMAP_SERVER"]
        port = int(env_vars["EMAIL_IMAP_PORT"])
        if port == 993:
            mail = imaplib.IMAP4_SSL(server, port)
        else:
            mail = imaplib.IMAP4(server, port)
            mail.starttls()
        mail.login(env_vars["EMAIL_ADDRESS"], env_vars["EMAIL_PASSWORD"])
        mail.select("INBOX")
        status, messages = mail.search(None, "ALL")
        count = len(messages[0].split()) if status == "OK" and messages[0] else 0
        mail.logout()
        return True, f"Inbox: {count} messages"
    except imaplib.IMAP4.error as e:
        return False, f"IMAP auth failed: {e}"
    except Exception as e:
        return False, f"Connection error: {e}"


def _check_pygithub_lib() -> bool:
    try:
        from github import Github  # noqa: F401

        return True
    except ImportError:
        return False


def _check_slack_sdk_lib() -> bool:
    try:
        from slack_sdk import WebClient  # noqa: F401

        return True
    except ImportError:
        return False


def _check_google_libs() -> bool:
    """Check if Google API libraries are installed."""
    try:
        import google.oauth2.credentials  # noqa: F401
        import google_auth_oauthlib.flow  # noqa: F401
        import googleapiclient.discovery  # noqa: F401

        return True
    except ImportError:
        return False


def _check_twilio_lib() -> bool:
    try:
        from twilio.rest import Client  # noqa: F401

        return True
    except ImportError:
        return False


def _test_http_service(
    url: str,
    *,
    headers: dict | None = None,
    params: dict | None = None,
    auth: tuple[str, str] | None = None,
    method: str = "GET",
) -> tuple[bool, str]:
    """Reusable HTTP connectivity test.  Returns (success, message)."""
    import httpx

    try:
        with httpx.Client(timeout=10, verify=True) as client:
            resp = client.request(
                method,
                url,
                headers=headers or {},
                params=params,
                auth=auth,
            )
        if resp.status_code < 400:
            return True, f"OK (HTTP {resp.status_code})"
        if resp.status_code in (401, 403):
            return (
                False,
                f"HTTP {resp.status_code}: Authentication failed. Check your token or credentials.",
            )
        if resp.status_code == 404:
            return False, "HTTP 404: Endpoint not found. Check the URL path."
        if resp.status_code >= 500:
            return False, f"HTTP {resp.status_code}: Server error. Check service logs."
        return False, f"HTTP {resp.status_code}: {resp.text[:200]}"
    except httpx.ConnectError as e:
        err = str(e).lower()
        if "ssl" in err or "certificate" in err:
            return False, "SSL/TLS error: Check URL scheme (http vs https) and certificates."
        if "refused" in err:
            return False, "Connection refused: Is the service running? Check URL and port."
        if "resolve" in err or "name" in err:
            return False, "DNS error: Cannot resolve hostname. Check for typos."
        return False, f"Connection failed: {e}"
    except Exception as e:
        return False, f"Error: {e}"


def _check_tcp_port(host: str, port: int, timeout: float = 2.0) -> bool:
    """Check if a TCP port is reachable."""
    import socket

    try:
        with socket.create_connection((host, port), timeout=timeout):
            return True
    except (OSError, TimeoutError):
        return False
