#!/usr/bin/env python3
# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License

"""
Google Workspace Onboarding  (v1.1.34 — Phase 3)
================================================

Completes Google auth from scratch on any machine, including headless
remote servers, without the user reading a single doc.

8 stages:
    1. Install Google libraries
    2. Locate / accept credentials.json
    3. Validate credentials type (Desktop app required, not Web app)
    4. API enablement guide (Calendar, Drive, Gmail, People)
    5. OAuth consent screen reminder
    6. Authorize each service — resume from incomplete, skip authorized
    7. Write test — create + delete a test doc (no orphaned files)
    8. Final report — exactly which Familiar skills are now unlocked

Headless / remote server support:
    When no local browser is available, the auth URL is printed.
    The user opens it on any machine, approves, then either:
      a) The localhost redirect is tunnelled (same machine or port-forwarded), or
      b) The user pastes the full redirect URL back at the prompt.

Usage:
    python -m familiar.onboard.google_setup
    python -m familiar.onboard.google_setup --services calendar drive
    python -m familiar.onboard.google_setup --skip-test
    python -m familiar.onboard.google_setup --creds /path/to/credentials.json
"""

from __future__ import annotations

import argparse
import http.server
import json
import logging
import os
import shutil
import socket
import subprocess
import sys
import threading
import time
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# ── Paths ─────────────────────────────────────────────────────────────────────

DATA_DIR = Path.home() / ".familiar" / "data"
CREDENTIALS_FILE = DATA_DIR / "google_credentials.json"

# Token file per service — must match the paths expected by each skill
SERVICE_TOKENS = {
    "calendar": DATA_DIR / "google_token.json",
    "drive": DATA_DIR / "google_token_drive.json",
    "gmail": DATA_DIR / "google_token_gmail.json",
    "contacts": DATA_DIR / "google_token_contacts.json",
}

# OAuth scopes per service — must match what the skill uses
SERVICE_SCOPES = {
    "calendar": ["https://www.googleapis.com/auth/calendar"],
    "drive": [
        "https://www.googleapis.com/auth/drive.file",
        "https://www.googleapis.com/auth/drive.readonly",
    ],
    "gmail": [
        "https://www.googleapis.com/auth/gmail.readonly",
        "https://www.googleapis.com/auth/gmail.send",
        "https://www.googleapis.com/auth/gmail.modify",
    ],
    "contacts": ["https://www.googleapis.com/auth/contacts.readonly"],
}

# Human-readable labels
SERVICE_LABELS = {
    "calendar": "Google Calendar",
    "drive": "Google Drive & Docs",
    "gmail": "Gmail",
    "contacts": "Google Contacts",
}

# Which Familiar skills each service unlocks
SERVICE_SKILLS = {
    "calendar": ["calendar_today", "create_event", "delete_event", "check_availability"],
    "drive": [
        "drive_create_doc",
        "drive_append_doc",
        "drive_update_doc",
        "drive_search",
        "drive_read",
        "drive_list",
        "drive_create_sheet",
    ],
    "gmail": ["send_email (Gmail API)", "triage_inbox (Gmail path)"],
    "contacts": ["contacts_search", "contacts_list"],
}

# APIs that must be enabled in Cloud Console
REQUIRED_APIS = [
    ("Google Calendar API", "calendar-json.googleapis.com"),
    ("Google Drive API", "www.googleapis.com/discovery/v1/apis/drive"),
    ("Gmail API", "gmail.googleapis.com"),
    ("Google People API", "people.googleapis.com"),
    ("Google Docs API", "docs.googleapis.com"),
    ("Google Sheets API", "sheets.googleapis.com"),
]


# ── Terminal colours ───────────────────────────────────────────────────────────


class C:
    BOLD = "\033[1m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    RED = "\033[91m"
    CYAN = "\033[96m"
    DIM = "\033[2m"
    RESET = "\033[0m"

    @staticmethod
    def ok(msg):
        print(f"  {C.GREEN}✓{C.RESET} {msg}")

    @staticmethod
    def warn(msg):
        print(f"  {C.YELLOW}⚠{C.RESET}  {msg}")

    @staticmethod
    def fail(msg):
        print(f"  {C.RED}✗{C.RESET} {msg}")

    @staticmethod
    def info(msg):
        print(f"  {C.CYAN}→{C.RESET} {msg}")

    @staticmethod
    def step(n, total, title):
        print(f"\n{C.BOLD}── Stage {n}/{total}: {title} ──────────────────────────────{C.RESET}")


def _yn(prompt: str, default: bool = True) -> bool:
    yn = "Y/n" if default else "y/N"
    resp = input(f"  {prompt} [{yn}]: ").strip().lower()
    if not resp:
        return default
    return resp.startswith("y")


def _pause(msg: str = "Press Enter to continue..."):
    input(f"\n  {C.DIM}{msg}{C.RESET}")


# ── Stage 1: Install Google libraries ─────────────────────────────────────────


def stage_install_libs() -> bool:
    """Install google-auth-oauthlib, google-auth-httplib2, google-api-python-client."""
    C.step(1, 8, "Install Google Libraries")

    packages = [
        "google-auth-oauthlib",
        "google-auth-httplib2",
        "google-api-python-client",
    ]

    missing = []
    for pkg in packages:
        _mod = pkg.replace("-", "_").split("_")[0]  # rough heuristic for module name
        try:
            if pkg == "google-auth-oauthlib":
                import google_auth_oauthlib  # noqa: F401
            elif pkg == "google-auth-httplib2":
                import google_auth_httplib2  # noqa: F401
            elif pkg == "google-api-python-client":
                import googleapiclient  # noqa: F401
        except ImportError:
            missing.append(pkg)

    if not missing:
        C.ok("All Google libraries already installed.")
        return True

    C.info(f"Installing: {', '.join(missing)}")
    result = subprocess.run(
        [sys.executable, "-m", "pip", "install", "--break-system-packages", "-q"] + missing,
        timeout=180,
    )
    if result.returncode == 0:
        C.ok("Google libraries installed.")
        return True
    else:
        C.fail("pip install failed. Run manually:")
        print(f"\n    pip install {' '.join(missing)} --break-system-packages\n")
        return False


# ── Stage 2: Locate credentials.json ──────────────────────────────────────────


def stage_locate_credentials(creds_override: Optional[Path] = None) -> bool:
    """Find or accept credentials.json and place it in DATA_DIR."""
    C.step(2, 8, "Google OAuth Credentials")

    DATA_DIR.mkdir(parents=True, exist_ok=True)

    if creds_override and creds_override.exists():
        if creds_override != CREDENTIALS_FILE:
            shutil.copy2(creds_override, CREDENTIALS_FILE)
            CREDENTIALS_FILE.chmod(0o600)
            C.ok(f"Copied credentials from {creds_override}")
        else:
            C.ok("Credentials file already in place.")
        return True

    if CREDENTIALS_FILE.exists():
        C.ok(f"Found: {CREDENTIALS_FILE}")
        return True

    # Not found — guide the user
    print(f"""
  {C.BOLD}credentials.json not found.{C.RESET}

  You need to create a Google Cloud project and download OAuth credentials.
  This takes about 5 minutes.

  {C.CYAN}Step-by-step:{C.RESET}

    1. Go to:  https://console.cloud.google.com/
    2. Create a new project — name it "Familiar" (or any name)
    3. Left sidebar → "APIs & Services" → "Credentials"
    4. Click "+ CREATE CREDENTIALS" → "OAuth client ID"
    5. Application type: {C.BOLD}Desktop app{C.RESET}  ← this is critical
    6. Name: "Familiar" → Create
    7. Click the download icon (↓) to download the JSON file

  {C.CYAN}Then either:{C.RESET}
    a) Place the file here: {C.BOLD}{CREDENTIALS_FILE}{C.RESET}
    b) Or enter its full path below
""")

    while True:
        path_str = input("  Path to credentials.json (or press Enter to check again): ").strip()

        if not path_str:
            if CREDENTIALS_FILE.exists():
                C.ok(f"Found: {CREDENTIALS_FILE}")
                return True
            C.warn("Still not found. Please place the file and try again.")
            continue

        src = Path(path_str).expanduser().resolve()
        if not src.exists():
            C.fail(f"File not found: {src}")
            continue

        if src != CREDENTIALS_FILE:
            shutil.copy2(src, CREDENTIALS_FILE)
            CREDENTIALS_FILE.chmod(0o600)
            C.ok(f"Copied to {CREDENTIALS_FILE}")

        return True


# ── Stage 3: Validate credentials type ────────────────────────────────────────


def stage_validate_credentials() -> bool:
    """
    Ensure credentials.json is a Desktop app client, not a Web app client.
    Web app clients use a different redirect_uri that doesn't work for CLI OAuth.
    """
    C.step(3, 8, "Validate Credentials Type")

    try:
        data = json.loads(CREDENTIALS_FILE.read_text())
    except (json.JSONDecodeError, OSError) as e:
        C.fail(f"Cannot read credentials.json: {e}")
        return False

    # Structure: {"installed": {...}} for Desktop app, {"web": {...}} for Web app
    if "installed" in data:
        client_id = data["installed"].get("client_id", "")
        C.ok("Desktop app credentials confirmed.")
        C.info(f"Client ID: {client_id[:30]}...")
        return True

    if "web" in data:
        C.fail("Wrong credentials type: this is a Web app client.")
        print(f"""
  {C.YELLOW}The credentials.json you provided is for a Web application, not a
  Desktop application. The Desktop app type is required for CLI OAuth.{C.RESET}

  {C.CYAN}Fix:{C.RESET}
    1. Go to: https://console.cloud.google.com/apis/credentials
    2. Find the client you downloaded and delete it (or keep it for other uses)
    3. Click "+ CREATE CREDENTIALS" → "OAuth client ID"
    4. Application type: {C.BOLD}Desktop app{C.RESET}
    5. Download the new JSON and run this script again

  The new file will be saved to:
    {CREDENTIALS_FILE}
""")
        # Offer to clear the bad file
        if _yn("Remove the invalid credentials.json now so you can replace it?", True):
            CREDENTIALS_FILE.unlink()
            C.info("Removed. Place the correct Desktop app credentials.json and re-run.")
        return False

    # Unexpected format
    keys = list(data.keys())
    C.fail(f"Unrecognised credentials format. Top-level keys: {keys}")
    C.info("Expected 'installed' (Desktop app) or 'web' (Web app).")
    return False


# ── Stage 4: API enablement guide ─────────────────────────────────────────────


def stage_api_enablement(services: list[str]) -> bool:
    """
    Remind the user to enable the required APIs.
    We can't do this programmatically without domain-wide delegation, so we
    guide them through the Cloud Console UI.
    """
    C.step(4, 8, "Enable Google APIs")

    needed = set()
    if "calendar" in services:
        needed.update(["Google Calendar API", "Google Calendar API"])
    if "drive" in services:
        needed.update(["Google Drive API", "Google Docs API", "Google Sheets API"])
    if "gmail" in services:
        needed.add("Gmail API")
    if "contacts" in services:
        needed.add("Google People API")

    # Get project ID from credentials
    try:
        data = json.loads(CREDENTIALS_FILE.read_text())
        project_id = data.get("installed", {}).get("project_id", "YOUR_PROJECT")
    except Exception:
        project_id = "YOUR_PROJECT"

    enable_url = f"https://console.cloud.google.com/apis/library?project={project_id}"

    print("""
  The following APIs must be enabled in your Google Cloud project:
""")
    api_set = []
    if "calendar" in services:
        api_set.append("  • Google Calendar API")
    if "drive" in services:
        api_set.append("  • Google Drive API")
        api_set.append("  • Google Docs API")
        api_set.append("  • Google Sheets API")
    if "gmail" in services:
        api_set.append("  • Gmail API")
    if "contacts" in services:
        api_set.append("  • Google People API")

    for line in api_set:
        print(f"{C.CYAN}{line}{C.RESET}")

    print(f"""
  {C.CYAN}Enable them here:{C.RESET}
    {enable_url}

  Search for each API name, click it, then click "{C.BOLD}Enable{C.RESET}".
  If already enabled, the button will say "Manage" — that's fine.
""")

    _pause("Press Enter once you've enabled the APIs (or if they're already on)...")
    C.ok("API enablement confirmed.")
    return True


# ── Stage 5: OAuth consent screen ─────────────────────────────────────────────


def stage_consent_screen() -> bool:
    """Remind the user to configure the OAuth consent screen."""
    C.step(5, 8, "OAuth Consent Screen")

    try:
        data = json.loads(CREDENTIALS_FILE.read_text())
        project_id = data.get("installed", {}).get("project_id", "YOUR_PROJECT")
    except Exception:
        project_id = "YOUR_PROJECT"

    consent_url = f"https://console.cloud.google.com/apis/credentials/consent?project={project_id}"

    print(f"""
  The OAuth consent screen controls what users see when they authorise Familiar.

  {C.CYAN}Required settings:{C.RESET}
    • User type: {C.BOLD}External{C.RESET} (even if you're the only user)
    • App name: "Familiar" (or any name)
    • User support email: your email
    • Publishing status: {C.BOLD}Testing{C.RESET} (you don't need production)
    • Test users: add your Google account email

  {C.CYAN}Configure here:{C.RESET}
    {consent_url}

  {C.DIM}If you've already configured this, skip ahead.{C.RESET}
""")

    _pause("Press Enter once the consent screen is configured (or if it's already done)...")
    C.ok("Consent screen confirmed.")
    return True


# ── Stage 6: Authorise each service ───────────────────────────────────────────


def _is_headless() -> bool:
    """Detect if we're running without a local browser (SSH, Docker, etc.)."""
    # If DISPLAY is set we have a display server (Linux/X11)
    if os.environ.get("DISPLAY"):
        return False
    # macOS always has a browser
    if sys.platform == "darwin":
        return False
    # WSL / Windows Subsystem for Linux — browser available via wslview
    if "microsoft" in (
        Path("/proc/version").read_text().lower() if Path("/proc/version").exists() else ""
    ):
        return False
    return True


def _find_free_port() -> int:
    with socket.socket() as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


def _run_local_server(port: int, timeout: int = 300) -> Optional[str]:
    """
    Start a one-shot localhost redirect server.
    Returns the 'code' query param from the redirect, or None on timeout/error.
    """
    result: dict = {}

    class _Handler(http.server.BaseHTTPRequestHandler):
        def do_GET(self):
            params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
            result["code"] = params.get("code", [None])[0]
            result["error"] = params.get("error", [None])[0]
            body = (
                b"<html><body style='font-family:sans-serif;text-align:center;"
                b"padding:60px'>"
                b"<h2>&#x2705; Authorization complete.</h2>"
                b"<p>You can close this tab and return to the terminal.</p>"
                b"</body></html>"
            )
            self.send_response(200)
            self.send_header("Content-Type", "text/html")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)

        def log_message(self, *args):
            pass  # suppress noise

    server = http.server.HTTPServer(("127.0.0.1", port), _Handler)
    server.timeout = timeout
    t = threading.Thread(target=server.handle_request, daemon=True)
    t.start()
    t.join(timeout + 2)
    server.server_close()
    return result.get("code")


def _open_browser(url: str):
    """Open a URL in the default browser, silently."""
    try:
        import webbrowser

        webbrowser.open(url)
    except Exception as e:
        logger.debug("Error suppressed: %s", e)


def _authorize_service(service: str) -> bool:
    """
    Run the OAuth flow for a single service.
    Returns True on success, False on failure.
    """
    try:
        from google.oauth2.credentials import Credentials
        from google_auth_oauthlib.flow import InstalledAppFlow
    except ImportError:
        C.fail("Google libraries not installed. Re-run stage 1.")
        return False

    token_file = SERVICE_TOKENS[service]
    scopes = SERVICE_SCOPES[service]
    label = SERVICE_LABELS[service]

    # Already authorised?
    if token_file.exists():
        try:
            creds = Credentials.from_authorized_user_file(str(token_file), scopes)
            if creds.valid:
                C.ok(f"{label} — already authorised, token valid.")
                return True
            if creds.expired and creds.refresh_token:
                from google.auth.transport.requests import Request

                creds.refresh(Request())
                token_file.write_text(creds.to_json())
                C.ok(f"{label} — token refreshed.")
                return True
        except Exception:
            pass  # Token corrupt or expired without refresh — re-authorise

    print(f"\n  {C.BOLD}Authorising {label}...{C.RESET}")

    flow = InstalledAppFlow.from_client_secrets_file(str(CREDENTIALS_FILE), scopes=scopes)

    port = _find_free_port()
    redirect_uri = f"http://localhost:{port}"
    flow.redirect_uri = redirect_uri
    auth_url, _ = flow.authorization_url(prompt="consent", access_type="offline")

    headless = _is_headless()

    if headless:
        print(f"""
  {C.YELLOW}Remote server detected — no local browser.{C.RESET}

  Open this URL on {C.BOLD}any browser{C.RESET} (your laptop, phone, etc.):

    {C.CYAN}{auth_url}{C.RESET}

  After approving, you will be redirected to:
    http://localhost:{port}/?code=...

  {C.DIM}If the redirect fails (page won't load), copy the full URL from
  your browser's address bar and paste it below.{C.RESET}
""")
    else:
        print(f"  Opening browser for {label} authorisation...")
        _open_browser(auth_url)
        print(f"\n  If the browser didn't open, visit:\n    {C.CYAN}{auth_url}{C.RESET}\n")

    # Start local server in background — catches auto-redirect
    code = None
    server_thread_result: dict = {}

    def _serve():
        server_thread_result["code"] = _run_local_server(port, timeout=300)

    t = threading.Thread(target=_serve, daemon=True)
    t.start()

    # Give the server a moment to start
    time.sleep(0.3)

    # Poll for auto-redirect completion (every 2s for up to 300s)
    if not headless:
        print(f"  {C.DIM}Waiting for browser redirect...  (Ctrl+C to enter URL manually){C.RESET}")
        for _ in range(150):
            time.sleep(2)
            if not t.is_alive():
                code = server_thread_result.get("code")
                break
            if server_thread_result.get("code"):
                code = server_thread_result["code"]
                break

    if not code:
        # Headless or auto-redirect timed out — ask for pasted URL
        while not code:
            raw = input(
                f"\n  Paste the full redirect URL (http://localhost:{port}/?code=...) "
                f"or the auth code alone:\n  > "
            ).strip()
            if not raw:
                C.warn("No input. Press Ctrl+C to skip this service.")
                continue
            # Accept full URL or bare code
            if raw.startswith("http"):
                params = urllib.parse.parse_qs(urllib.parse.urlparse(raw).query)
                code = params.get("code", [None])[0]
                err = params.get("error", [None])[0]
                if err:
                    C.fail(f"OAuth error in redirect: {err}")
                    return False
                if not code:
                    C.fail("Couldn't extract code from URL. Paste the full URL including '?code='")
                    continue
            else:
                code = raw  # bare code

    if not code:
        C.fail(f"No auth code received for {label}. Skipping.")
        return False

    # Exchange code for token
    try:
        flow.fetch_token(code=code)
        creds = flow.credentials
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        token_file.write_text(creds.to_json())
        token_file.chmod(0o600)
        C.ok(f"{label} authorised. Token saved to {token_file.name}")
        return True
    except Exception as e:
        C.fail(f"Token exchange failed for {label}: {e}")
        C.info("Check that the code hasn't expired (they expire in ~10 minutes).")
        return False


def stage_authorize_services(services: list[str]) -> dict[str, bool]:
    """Authorize each requested service. Returns {service: bool} results."""
    C.step(6, 8, "Authorise Google Services")

    results: dict[str, bool] = {}
    for svc in services:
        results[svc] = _authorize_service(svc)

    return results


# ── Stage 7: Write test ────────────────────────────────────────────────────────


def stage_write_test(auth_results: dict[str, bool]) -> bool:
    """
    If Drive is authorised, create a test Google Doc and immediately delete it.
    No orphaned files.
    """
    C.step(7, 8, "Write Test")

    if not auth_results.get("drive"):
        C.info("Drive not authorised — skipping write test.")
        return True

    print("  Creating a test Google Doc to verify write access...")

    try:
        from google.oauth2.credentials import Credentials
        from googleapiclient.discovery import build

        token_file = SERVICE_TOKENS["drive"]
        creds = Credentials.from_authorized_user_file(str(token_file), SERVICE_SCOPES["drive"])

        # Refresh if needed
        if creds.expired and creds.refresh_token:
            from google.auth.transport.requests import Request

            creds.refresh(Request())

        docs_svc = build("docs", "v1", credentials=creds, cache_discovery=False)
        drive_svc = build("drive", "v3", credentials=creds, cache_discovery=False)

        # Create test doc
        doc = (
            docs_svc.documents()
            .create(body={"title": "[Familiar write test — will be deleted]"})
            .execute()
        )
        doc_id = doc["documentId"]
        C.ok(f"Test doc created (ID: {doc_id[:16]}...)")

        # Verify it exists
        drive_svc.files().get(fileId=doc_id, fields="id, name").execute()
        C.ok("Doc confirmed readable via Drive API.")

        # Delete it — permanent, no trash
        drive_svc.files().delete(fileId=doc_id).execute()
        C.ok("Test doc deleted. No orphaned files.")

        return True

    except Exception as e:
        C.fail(f"Write test failed: {e}")
        C.info("Drive authorisation may have insufficient scope. Re-run to re-authorise Drive.")
        return False


# ── Stage 8: Final report ──────────────────────────────────────────────────────


def stage_final_report(auth_results: dict[str, bool], write_test_ok: bool):
    """Print a clear summary of which skills are now unlocked."""
    C.step(8, 8, "Final Report")

    unlocked = []
    locked = []

    for svc, ok in auth_results.items():
        skills = SERVICE_SKILLS.get(svc, [])
        label = SERVICE_LABELS[svc]
        if ok:
            unlocked.append((label, skills))
        else:
            locked.append((label, skills))

    print()
    if unlocked:
        print(f"  {C.GREEN}{C.BOLD}Unlocked skills:{C.RESET}")
        for label, skills in unlocked:
            print(f"    {C.GREEN}✓{C.RESET}  {label}")
            for skill in skills:
                print(f"      - {skill}")

    if write_test_ok and auth_results.get("drive"):
        print(f"    {C.GREEN}✓{C.RESET}  Drive write access verified")

    if locked:
        print(f"\n  {C.YELLOW}{C.BOLD}Not yet authorised:{C.RESET}")
        for label, skills in locked:
            print(f"    {C.YELLOW}⬜{C.RESET}  {label}")
            for skill in skills:
                print(f"      - {skill}")
        print(f"\n  {C.DIM}Re-run this script to authorise the remaining services.{C.RESET}")

    if not unlocked:
        print(f"  {C.RED}No services were authorised. Nothing is unlocked yet.{C.RESET}")
        print("  Run this script again and complete the OAuth steps.")
        return

    # Next steps
    print(f"""
  {C.BOLD}Ready to use.{C.RESET}

  In Telegram, try:
    "What's on my calendar today?"
    "Create a doc called Meeting Notes"
    "Read my latest emails"

  Token files stored at:
    {DATA_DIR}/
""")

    if not auth_results.get("drive") or not write_test_ok:
        print(
            f"  {C.YELLOW}⚠  Drive write test did not complete. "
            f"Docs skills may not work. Re-run to retry.{C.RESET}\n"
        )


# ── Partial-restart support ────────────────────────────────────────────────────


def _already_authorised(service: str) -> bool:
    """True if the service has a token file with a working credential.

    Previously this returned True if a refresh_token merely *existed*,
    which masked revoked/expired refresh tokens (e.g. Google "Testing"
    mode 7-day expiry).  Now we actually attempt a refresh to confirm
    the token is usable.
    """
    token_file = SERVICE_TOKENS.get(service)
    if not token_file or not token_file.exists():
        return False
    try:
        from google.oauth2.credentials import Credentials

        creds = Credentials.from_authorized_user_file(str(token_file), SERVICE_SCOPES[service])
        if creds.valid:
            return True
        # Token expired — try to refresh it
        if creds.expired and creds.refresh_token:
            from google.auth.transport.requests import Request

            creds.refresh(Request())
            # Persist the refreshed token so we don't refresh again next time
            token_file.write_text(creds.to_json())
            return True
        return False
    except Exception:
        return False


# ── Main entry point ───────────────────────────────────────────────────────────


def run_google_setup(
    services: Optional[list[str]] = None,
    creds_override: Optional[Path] = None,
    skip_test: bool = False,
) -> bool:
    """
    Run the full 8-stage Google onboarding flow.

    services: list of service names to authorise (default: all)
    creds_override: path to credentials.json to use instead of the default
    skip_test: skip the write test (stage 7)

    Returns True if at least one service was successfully authorised.
    """
    if services is None:
        services = list(SERVICE_TOKENS.keys())

    # Normalise
    services = [s.lower() for s in services]
    unknown = [s for s in services if s not in SERVICE_TOKENS]
    if unknown:
        C.fail(f"Unknown service(s): {', '.join(unknown)}")
        C.info(f"Valid options: {', '.join(SERVICE_TOKENS.keys())}")
        return False

    # ── Banner ──
    print(f"""
{C.BOLD}{"═" * 58}

    🔑  Familiar — Google Workspace Setup

    This script connects Calendar, Drive, Gmail, and Contacts.
    It takes about 5 minutes on a fresh machine.

{"═" * 58}{C.RESET}
""")

    # Check which services still need authorisation
    needs_auth = [s for s in services if not _already_authorised(s)]
    already = [s for s in services if _already_authorised(s)]

    if already:
        print(f"  Already authorised: {', '.join(SERVICE_LABELS[s] for s in already)}")
    if needs_auth:
        print(f"  Need authorisation: {', '.join(SERVICE_LABELS[s] for s in needs_auth)}")
    else:
        print(f"\n  {C.GREEN}All requested services are already authorised!{C.RESET}")
        if not skip_test:
            auth_results = {s: True for s in services}
            wt = stage_write_test(auth_results)
            stage_final_report(auth_results, wt)
        return True

    print()

    # ── Stage 1: Libraries ──
    if not stage_install_libs():
        C.fail("Cannot continue without Google libraries.")
        return False

    # ── Stage 2: Credentials ──
    if not stage_locate_credentials(creds_override):
        C.fail("Cannot continue without credentials.json.")
        return False

    # ── Stage 3: Validate type ──
    if not stage_validate_credentials():
        C.fail("Cannot continue with wrong credentials type.")
        return False

    # ── Stage 4: API enablement ──
    stage_api_enablement(needs_auth)

    # ── Stage 5: Consent screen ──
    stage_consent_screen()

    # ── Stage 6: Authorise ──
    new_results = stage_authorize_services(needs_auth)

    # Merge with already-authorised
    auth_results = {s: True for s in already}
    auth_results.update(new_results)

    # ── Stage 7: Write test ──
    write_test_ok = True
    if not skip_test:
        write_test_ok = stage_write_test(auth_results)

    # ── Stage 8: Report ──
    stage_final_report(auth_results, write_test_ok)

    any_success = any(auth_results.values())
    return any_success


def main():
    parser = argparse.ArgumentParser(
        prog="python -m familiar.onboard.google_setup",
        description="Connect Google Workspace to Familiar (Calendar, Drive, Gmail, Contacts).",
    )
    parser.add_argument(
        "--services",
        nargs="+",
        metavar="SERVICE",
        help=f"Services to authorise (default: all). Choices: {', '.join(SERVICE_TOKENS)}",
    )
    parser.add_argument(
        "--creds",
        type=Path,
        metavar="FILE",
        help="Path to credentials.json (default: ~/.familiar/data/google_credentials.json)",
    )
    parser.add_argument(
        "--skip-test",
        action="store_true",
        help="Skip the Drive write test (stage 7)",
    )
    args = parser.parse_args()

    try:
        success = run_google_setup(
            services=args.services,
            creds_override=args.creds,
            skip_test=args.skip_test,
        )
        sys.exit(0 if success else 1)
    except KeyboardInterrupt:
        print(f"\n\n  {C.YELLOW}Setup interrupted.{C.RESET}")
        print("  Re-run to continue from where you left off. Completed authorisations are saved.\n")
        sys.exit(1)


if __name__ == "__main__":
    main()
