# 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

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

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

# 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_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
