#!/usr/bin/env python3
"""
TradiesVoice — GHL Pipeline Runner
====================================
Unified deployment runner for TradiesVoice GHL app modules.
Deploys any combination of the 6 TradiesVoice apps to a GHL sub-account.

Usage:
  python ghl_pipeline_runner.py --client-data onboarding.json --apps website,mctb,review_generator
  python ghl_pipeline_runner.py --client-data onboarding.json --plan growth_bundle
  python ghl_pipeline_runner.py --client-data onboarding.json --plan full_suite

Plans:
  website          App 1 only ($297/mo)
  starter_bundle   Website + MCTB + Voice Widget ($497/mo)
  growth_bundle    Website + MCTB + Voice Widget + Review Generator ($597/mo)
  full_suite       All 6 apps ($697/mo)

Individual apps:
  website, mctb, voice_widget, phone_agent, review_generator, review_display_widget

Environment Variables Required:
  GHL_AGENCY_API_KEY   — GHL Agency API key (Private Integration token)
  TELNYX_API_KEY       — Telnyx API key (for voice modules)

Optional:
  GHL_COMPANY_ID       — GHL Company ID (for sub-account creation)
  NETLIFY_TOKEN        — For site deployment
  GOOGLE_PLACES_KEY    — For Review Display Widget

TradiesVoice | ReceptionistAI | Genesis Build | 2026-02-24
"""

import argparse
import json
import logging
import os
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Optional

# ---------------------------------------------------------------------------
# Paths to existing deploy scripts
# ---------------------------------------------------------------------------

REPO_ROOT = Path(__file__).resolve().parent.parent
GHL_MODULES_ROOT = REPO_ROOT / "GHL_MODULES"
MCTB_DEPLOY_SCRIPT = GHL_MODULES_ROOT / "mctb_widget" / "ghl_mctb_deploy.py"
REVIEW_DEPLOY_SCRIPT = GHL_MODULES_ROOT / "review_generator" / "ghl_api_deploy.py"
PROVISION_SCRIPT = GHL_MODULES_ROOT / "mctb_widget" / "provision_telnyx_assistant.py"
WIDGET_HTML = GHL_MODULES_ROOT / "widget" / "tradie_widget.html"

# ---------------------------------------------------------------------------
# Plan definitions — maps plan name to list of app slugs
# ---------------------------------------------------------------------------

PLANS = {
    "website": ["website"],
    "starter_bundle": ["website", "mctb", "voice_widget"],
    "growth_bundle": ["website", "mctb", "voice_widget", "review_generator"],
    "full_suite": [
        "website",
        "mctb",
        "voice_widget",
        "phone_agent",
        "review_generator",
        "review_display_widget",
    ],
}

VALID_APPS = {
    "website",
    "mctb",
    "voice_widget",
    "phone_agent",
    "review_generator",
    "review_display_widget",
}

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------


def setup_logging(log_file: Optional[str] = None) -> logging.Logger:
    """Configure console + optional file logging."""
    logger = logging.getLogger("ghl_pipeline_runner")
    logger.setLevel(logging.DEBUG)

    fmt = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%H:%M:%S")

    ch = logging.StreamHandler(sys.stdout)
    ch.setLevel(logging.INFO)
    ch.setFormatter(fmt)
    logger.addHandler(ch)

    if log_file:
        fh = logging.FileHandler(log_file)
        fh.setLevel(logging.DEBUG)
        fh.setFormatter(fmt)
        logger.addHandler(fh)

    return logger


log = setup_logging(
    log_file=f"ghl_deploy_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.log"
)

# ---------------------------------------------------------------------------
# Credential helpers
# ---------------------------------------------------------------------------


def require_env(var: str) -> str:
    """Exit with a clear error if a required env var is missing."""
    val = os.environ.get(var)
    if not val:
        log.error(
            f"Required environment variable '{var}' is not set.\n"
            f"  Export it before running: set {var}=your_value_here"
        )
        sys.exit(1)
    return val


def get_ghl_api_key() -> str:
    """
    Returns GHL API key. Accepts either GHL_AGENCY_API_KEY or the legacy GHL_API_KEY.
    Standardises to GHL_AGENCY_API_KEY going forward.
    """
    key = os.environ.get("GHL_AGENCY_API_KEY") or os.environ.get("GHL_API_KEY")
    if not key:
        log.error(
            "GHL API key not found.\n"
            "  Set: export GHL_AGENCY_API_KEY=your_key_here\n"
            "  (Accepts either GHL_AGENCY_API_KEY or legacy GHL_API_KEY)"
        )
        sys.exit(1)
    return key


# ---------------------------------------------------------------------------
# Client data loader
# ---------------------------------------------------------------------------


def load_client_data(path: str) -> dict:
    """
    Load and validate client onboarding data from JSON file.

    Required fields:
      sub_account_id    — GHL location/sub-account ID
      business_name     — Client trading name
      owner_phone       — E.164 format (+61XXXXXXXXX)
      booking_link      — Online booking URL

    Optional fields (required for specific modules):
      google_review_link   — Required for review_generator
      services             — Required for voice_widget / phone_agent
      service_area         — Required for voice_widget / phone_agent
      hours                — Required for voice_widget / phone_agent
      escalation_number    — Required for voice_widget / phone_agent
      owner_name           — Required for voice_widget / phone_agent
      state_area_code      — For phone_agent number provisioning (e.g. "02", "07")
      google_place_id      — Required for review_display_widget
      widget_color         — Hex colour for widget branding (default: #1a2744)
    """
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        log.error(f"Client data file not found: {path}")
        sys.exit(1)
    except json.JSONDecodeError as e:
        log.error(f"Invalid JSON in client data file: {e}")
        sys.exit(1)

    required_base = ["sub_account_id", "business_name", "owner_phone", "booking_link"]
    missing = [k for k in required_base if not data.get(k)]
    if missing:
        log.error(f"Client data is missing required fields: {missing}")
        sys.exit(1)

    return data


# ---------------------------------------------------------------------------
# Module deployment functions
# ---------------------------------------------------------------------------


class DeploymentResult:
    """Tracks the outcome of a single module deployment."""

    def __init__(self, app: str):
        self.app = app
        self.success = False
        self.notes: list[str] = []
        self.manual_steps: list[str] = []

    def ok(self, note: str = "") -> None:
        self.success = True
        if note:
            self.notes.append(note)

    def fail(self, reason: str) -> None:
        self.success = False
        self.notes.append(f"FAILED: {reason}")

    def manual(self, step: str) -> None:
        self.manual_steps.append(step)


def deploy_website(client: dict) -> DeploymentResult:
    """
    App 1: Website module.
    Creates custom fields, pipeline, and sub-account values for the base website plan.

    Note: Extendly snapshot must be applied manually via GHL UI (one-time).
    Note: No existing deploy script for sub-account creation — manual or via Agency API.
    """
    result = DeploymentResult("website")
    log.info("[APP 1] Deploying Website module...")

    # Validate prerequisites
    sub_account_id = client["sub_account_id"]
    business_name = client["business_name"]

    # The website module does not yet have a full deploy script.
    # The Smart Widget (tradie_widget.html) is the embeddable component.
    # Pipeline creation follows the same pattern as MCTB (see ghl_mctb_deploy.py).

    log.info(f"  Sub-Account: {sub_account_id}")
    log.info(f"  Business: {business_name}")

    result.manual(
        "Apply Extendly Small Business Snapshot: "
        "GHL Agency UI > Snapshots > Apply to this sub-account"
    )
    result.manual(
        "Configure tradie_widget.html RAD_CONFIG with client values, "
        "then embed in client website per WIDGET_EMBED_GUIDE.md"
    )
    result.manual(
        "In GHL: set up booking intake workflow with Webhook trigger — "
        "copy webhook URL to ghlWebhookUrl in RAD_CONFIG"
    )

    # TODO: When sub-account creation script is built, call it here:
    # create_sub_account(business_name, owner_phone, ...)

    log.info("  [STUB] Website module: manual steps queued. See deployment summary.")
    result.ok("Website module staged — manual steps required (see summary)")
    return result


def deploy_mctb(client: dict) -> DeploymentResult:
    """
    App 2: Missed Call Text Back (MCTB) module.
    Calls the existing ghl_mctb_deploy.py script.
    """
    result = DeploymentResult("mctb")
    log.info("[APP 2] Deploying MCTB module...")

    required = ["sub_account_id", "business_name", "owner_phone", "booking_link"]
    missing = [k for k in required if not client.get(k)]
    if missing:
        result.fail(f"Missing required fields for MCTB: {missing}")
        return result

    if not MCTB_DEPLOY_SCRIPT.exists():
        result.fail(f"MCTB deploy script not found at: {MCTB_DEPLOY_SCRIPT}")
        return result

    env = os.environ.copy()
    # Normalise the API key env var for the subprocess
    if os.environ.get("GHL_AGENCY_API_KEY"):
        env["GHL_AGENCY_API_KEY"] = os.environ["GHL_AGENCY_API_KEY"]

    cmd = [
        sys.executable,
        str(MCTB_DEPLOY_SCRIPT),
        "--sub-account-id", client["sub_account_id"],
        "--business-name", client["business_name"],
        "--booking-link", client["booking_link"],
        "--owner-phone", client["owner_phone"],
    ]

    log.info(f"  Running: {' '.join(cmd[:3])} ... (truncated)")

    try:
        proc = subprocess.run(
            cmd,
            env=env,
            capture_output=True,
            text=True,
            timeout=120,
        )
        if proc.returncode == 0:
            log.info("  [OK] MCTB deploy script completed successfully")
            result.ok("Custom fields, pipeline, SMS templates, custom values deployed")
        else:
            log.error(f"  [FAIL] MCTB deploy script exited {proc.returncode}")
            log.error(f"  STDERR: {proc.stderr[:500]}")
            result.fail(f"Script exit code {proc.returncode}: {proc.stderr[:200]}")
            return result
    except subprocess.TimeoutExpired:
        result.fail("MCTB deploy script timed out after 120 seconds")
        return result
    except Exception as e:
        result.fail(f"Unexpected error running MCTB deploy script: {e}")
        return result

    result.manual(
        "WORKFLOW REQUIRED: Import MCTB snapshot OR build manually in GHL.\n"
        "  Spec: E:\\genesis-system\\GHL_MODULES\\mctb_widget\\MCTB_SNAPSHOT_SPEC.md\n"
        "  After import: verify workflow is Active, test with a missed call."
    )
    return result


def deploy_voice_widget(client: dict) -> DeploymentResult:
    """
    App 3: Voice Widget — Telnyx AI Assistant + Smart Widget embed.

    Calls provision_telnyx_assistant.py to create a Telnyx AI Assistant,
    upload the knowledge base, store the assistant ID in GHL, and output
    the widget embed code.
    """
    result = DeploymentResult("voice_widget")
    log.info("[APP 3] Deploying Voice Widget module...")

    required = ["sub_account_id", "business_name", "services",
                "service_area", "hours", "booking_link",
                "escalation_number", "owner_name"]
    missing = [k for k in required if not client.get(k)]
    if missing:
        log.warning(f"  Missing fields for Voice Widget: {missing}")
        log.warning("  Cannot auto-provision — add missing fields to client data and re-run.")
        result.manual(
            f"Voice Widget: fill in missing client fields before re-running: {missing}"
        )
        result.fail(f"Voice Widget skipped — missing fields: {missing}")
        return result

    telnyx_key = os.environ.get("TELNYX_API_KEY")
    if not telnyx_key:
        log.warning("  TELNYX_API_KEY not set — Telnyx provisioning skipped.")
        result.manual("Set TELNYX_API_KEY and re-run: export TELNYX_API_KEY=your_key_here")
        result.manual(
            "Voice Widget provisioning script: "
            f"{PROVISION_SCRIPT}"
        )
        result.ok("Voice Widget queued — set TELNYX_API_KEY and re-run")
        return result

    if not PROVISION_SCRIPT.exists():
        log.error(f"  Provisioning script not found: {PROVISION_SCRIPT}")
        result.fail(f"provision_telnyx_assistant.py missing: {PROVISION_SCRIPT}")
        return result

    # Build client-data JSON for the provision script
    provision_data = {
        "sub_account_id": client["sub_account_id"],
        "business_name": client["business_name"],
        "services": client.get("services", ""),
        "service_area": client.get("service_area", ""),
        "hours": client.get("hours", ""),
        "booking_link": client.get("booking_link", ""),
        "escalation_number": client.get("escalation_number", ""),
        "owner_name": client.get("owner_name", ""),
        "area_code": client.get("state_area_code", "07"),
        "widget_color": client.get("widget_color", "#1a1a2e"),
        "provision_number": client.get("provision_number", False),
        "existing_phone_number_id": client.get("existing_phone_number_id"),
    }

    # Write a temp JSON file for the subprocess
    import tempfile
    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".json", delete=False, encoding="utf-8"
    ) as tmp:
        json.dump(provision_data, tmp)
        tmp_path = tmp.name

    env = os.environ.copy()
    cmd = [sys.executable, str(PROVISION_SCRIPT), "--client-data", tmp_path]

    log.info(f"  Running: {' '.join(cmd)}")
    proc = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=120)

    # Clean up temp file
    try:
        Path(tmp_path).unlink()
    except OSError:
        pass

    if proc.stdout:
        for line in proc.stdout.splitlines():
            log.info(f"  | {line}")
    if proc.stderr:
        for line in proc.stderr.splitlines():
            log.warning(f"  | STDERR: {line}")

    if proc.returncode != 0:
        result.fail(
            f"provision_telnyx_assistant.py exited {proc.returncode}. "
            "Check TELNYX_API_KEY and client data."
        )
        return result

    result.manual(
        "WIDGET DEPLOYMENT (after Telnyx provisioning above):\n"
        "  1. Copy the embed code printed above\n"
        "  2. Paste into client website before </body>\n"
        "  3. OR embed tradie_widget.html full widget with RAD_CONFIG updated:\n"
        f"     {WIDGET_HTML}"
    )
    result.ok("Voice Widget provisioned — Telnyx AI Assistant created, embed code generated")
    return result


def deploy_phone_agent(client: dict) -> DeploymentResult:
    """
    App 4: Phone Voice Agent — Telnyx AI Assistant + dedicated AU phone number.

    Calls provision_telnyx_assistant.py with --provision-number flag to
    create the assistant AND order + assign a new AU phone number in one pass.
    """
    result = DeploymentResult("phone_agent")
    log.info("[APP 4] Deploying Phone Voice Agent module...")

    state_area_code = client.get("state_area_code", "07")
    telnyx_key = os.environ.get("TELNYX_API_KEY")

    required = ["sub_account_id", "business_name", "services",
                "service_area", "hours", "booking_link",
                "escalation_number", "owner_name"]
    missing = [k for k in required if not client.get(k)]

    if not telnyx_key:
        log.warning("  TELNYX_API_KEY not set — Phone Agent provisioning skipped.")
        result.manual(
            "PHONE AGENT PROVISIONING:\n"
            f"  1. Run provision_telnyx_assistant.py with --provision-number --area-code {state_area_code}\n"
            f"  Script: {PROVISION_SCRIPT}\n"
            "  2. Confirm number assigned in Telnyx portal (Numbers > My Numbers)\n"
            "  3. Configure Telnyx post-call webhook to n8n → GHL"
        )
        result.ok("Phone Agent queued — set TELNYX_API_KEY and re-run")
        return result

    if missing:
        log.warning(f"  Missing fields: {missing}")
        result.manual(
            f"Phone Agent: fill in missing client fields and re-run: {missing}"
        )
        result.fail(f"Phone Agent skipped — missing fields: {missing}")
        return result

    if not PROVISION_SCRIPT.exists():
        log.error(f"  Provisioning script not found: {PROVISION_SCRIPT}")
        result.fail(f"provision_telnyx_assistant.py missing: {PROVISION_SCRIPT}")
        return result

    provision_data = {
        "sub_account_id": client["sub_account_id"],
        "business_name": client["business_name"],
        "services": client.get("services", ""),
        "service_area": client.get("service_area", ""),
        "hours": client.get("hours", ""),
        "booking_link": client.get("booking_link", ""),
        "escalation_number": client.get("escalation_number", ""),
        "owner_name": client.get("owner_name", ""),
        "area_code": state_area_code,
        "widget_color": client.get("widget_color", "#1a1a2e"),
        "provision_number": True,   # Always provision a number for App 4
        "existing_phone_number_id": None,
    }

    import tempfile
    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".json", delete=False, encoding="utf-8"
    ) as tmp:
        json.dump(provision_data, tmp)
        tmp_path = tmp.name

    env = os.environ.copy()
    cmd = [sys.executable, str(PROVISION_SCRIPT), "--client-data", tmp_path]

    log.info(f"  Running: {' '.join(cmd)}")
    proc = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=180)

    try:
        Path(tmp_path).unlink()
    except OSError:
        pass

    if proc.stdout:
        for line in proc.stdout.splitlines():
            log.info(f"  | {line}")
    if proc.stderr:
        for line in proc.stderr.splitlines():
            log.warning(f"  | STDERR: {line}")

    if proc.returncode != 0:
        result.fail(
            f"provision_telnyx_assistant.py exited {proc.returncode}. "
            "Check TELNYX_API_KEY, area code availability, and client data."
        )
        return result

    result.manual(
        "POST-PROVISIONING:\n"
        "  1. Verify number in Telnyx portal → Numbers > My Numbers\n"
        "  2. Test call: dial the provisioned number and confirm AIVA answers\n"
        "  3. Configure Telnyx post-call webhook to n8n → GHL contact update workflow\n"
        "  4. Add GHL custom fields: ai_call_duration, ai_call_outcome, ai_call_recording_url"
    )
    result.ok(
        "Phone Agent provisioned — Telnyx AI Assistant created, AU number ordered and assigned"
    )
    return result


def deploy_review_generator(client: dict) -> DeploymentResult:
    """
    App 5: Review Generator (Reputation Rocket).
    Calls the existing ghl_api_deploy.py script.
    """
    result = DeploymentResult("review_generator")
    log.info("[APP 5] Deploying Review Generator module...")

    required = ["sub_account_id", "google_review_link", "business_name"]
    missing = [k for k in required if not client.get(k)]
    if missing:
        result.fail(f"Missing required fields for Review Generator: {missing}")
        return result

    if not REVIEW_DEPLOY_SCRIPT.exists():
        result.fail(f"Review Generator deploy script not found at: {REVIEW_DEPLOY_SCRIPT}")
        return result

    # The review generator script uses GHL_API_KEY (legacy naming)
    # We normalise here — set GHL_API_KEY from GHL_AGENCY_API_KEY if not already set
    env = os.environ.copy()
    if not env.get("GHL_API_KEY") and env.get("GHL_AGENCY_API_KEY"):
        env["GHL_API_KEY"] = env["GHL_AGENCY_API_KEY"]

    rating_page_url = client.get(
        "rating_page_url",
        "[RATING_PAGE_URL — UPDATE AFTER GHL FUNNEL IS PUBLISHED]"
    )

    cmd = [
        sys.executable,
        str(REVIEW_DEPLOY_SCRIPT),
        "--sub-account-id", client["sub_account_id"],
        "--google-review-link", client["google_review_link"],
        "--business-name", client["business_name"],
        "--rating-page-url", rating_page_url,
    ]

    log.info(f"  Running: {' '.join(cmd[:3])} ... (truncated)")

    try:
        proc = subprocess.run(
            cmd,
            env=env,
            capture_output=True,
            text=True,
            timeout=120,
        )
        if proc.returncode == 0:
            log.info("  [OK] Review Generator deploy script completed successfully")
            result.ok("Custom fields, pipeline, SMS templates, custom values deployed")
        else:
            log.error(f"  [FAIL] Review Generator deploy script exited {proc.returncode}")
            log.error(f"  STDERR: {proc.stderr[:500]}")
            result.fail(f"Script exit code {proc.returncode}: {proc.stderr[:200]}")
            return result
    except subprocess.TimeoutExpired:
        result.fail("Review Generator deploy script timed out after 120 seconds")
        return result
    except Exception as e:
        result.fail(f"Unexpected error running Review Generator deploy script: {e}")
        return result

    result.manual(
        "WORKFLOWS REQUIRED (3 total) — import snapshot OR build manually:\n"
        "  RR-01: Review Request workflow (webhook/tag trigger → 2hr wait → SMS)\n"
        "  RR-02: 5-Star Filter workflow (form submission trigger → rating branch)\n"
        "  RR-03: Google Review Follow-Up (positive-feedback tag → 48hr wait → SMS)\n"
        "  Spec: E:\\genesis-system\\GHL_MODULES\\review_generator\\SNAPSHOT_SPEC.md"
    )
    result.manual(
        "RATING LANDING PAGE FUNNEL REQUIRED:\n"
        "  Build in GHL: Sites > Funnels > New Funnel\n"
        "  Slug: rate-[business-name-slug]\n"
        "  After publish: update rating_page_url in SMS Template 'RR - Review Request'\n"
        "  Spec: E:\\genesis-system\\GHL_MODULES\\review_generator\\SNAPSHOT_SPEC.md (Section 4)"
    )
    return result


def deploy_review_display_widget(client: dict) -> DeploymentResult:
    """
    App 6: Review Display Widget — Google reviews carousel embed.

    NOT YET BUILT — this module has no existing implementation.
    This function queues the required manual steps and logs the build gap.
    """
    result = DeploymentResult("review_display_widget")
    log.info("[APP 6] Deploying Review Display Widget module...")

    google_place_id = client.get("google_place_id")
    if not google_place_id:
        log.warning("  google_place_id not provided in client data.")
        result.manual("Get client's Google Place ID from GBP Dashboard or Google Maps URL")

    log.warning("  [NOT BUILT] Review Display Widget has no implementation yet.")
    log.warning("  See GHL_APP_PIPELINE_SPEC.md — App 6 — for build requirements.")

    result.manual(
        "BUILD REQUIRED: reviews.js widget does not exist yet.\n"
        "  Build: vanilla JS widget, Google Places API integration, CDN deploy\n"
        f"  Once built, embed: <script src='https://tradiesvoice.com.au/widget/reviews.js' "
        f"data-place-id='{google_place_id or 'PLACE_ID_PLACEHOLDER'}'></script>"
    )
    result.ok("Review Display Widget staged — build required (see manual steps)")
    return result


# ---------------------------------------------------------------------------
# Deployment orchestrator
# ---------------------------------------------------------------------------

DEPLOYERS = {
    "website": deploy_website,
    "mctb": deploy_mctb,
    "voice_widget": deploy_voice_widget,
    "phone_agent": deploy_phone_agent,
    "review_generator": deploy_review_generator,
    "review_display_widget": deploy_review_display_widget,
}


def run_pipeline(apps: list[str], client: dict) -> list[DeploymentResult]:
    """Execute deployment for each app in order, collecting results."""
    results = []
    total = len(apps)

    log.info(f"\n{'='*56}")
    log.info(f"  TradiesVoice GHL Pipeline Runner")
    log.info(f"{'='*56}")
    log.info(f"  Client:      {client['business_name']}")
    log.info(f"  Sub-Account: {client['sub_account_id']}")
    log.info(f"  Apps ({total}):    {', '.join(apps)}")
    log.info(f"  Timestamp:   {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC")
    log.info(f"{'='*56}\n")

    for i, app in enumerate(apps, 1):
        log.info(f"--- [{i}/{total}] {app.upper()} ---")

        if app not in DEPLOYERS:
            log.error(f"  Unknown app: '{app}'. Valid apps: {sorted(VALID_APPS)}")
            r = DeploymentResult(app)
            r.fail(f"Unknown app identifier: {app}")
            results.append(r)
            continue

        try:
            r = DEPLOYERS[app](client)
        except Exception as e:
            log.exception(f"  Unhandled exception during {app} deployment: {e}")
            r = DeploymentResult(app)
            r.fail(f"Unhandled exception: {e}")

        results.append(r)
        status = "OK" if r.success else "FAIL"
        log.info(f"  [{status}] {app}")
        for note in r.notes:
            log.info(f"       {note}")
        time.sleep(0.3)  # Brief pause between modules

    return results


def print_summary(results: list[DeploymentResult], client: dict) -> None:
    """Print final deployment summary with status per module and all manual steps."""
    total = len(results)
    passed = sum(1 for r in results if r.success)
    failed = total - passed
    all_manual: list[tuple[str, str]] = []

    for r in results:
        for step in r.manual_steps:
            all_manual.append((r.app, step))

    log.info(f"\n{'='*56}")
    log.info(f"  DEPLOYMENT SUMMARY — {client['business_name']}")
    log.info(f"{'='*56}")
    log.info(f"  Modules:  {total}  |  OK: {passed}  |  FAIL: {failed}")
    log.info(f"{'='*56}")

    for r in results:
        status = "OK  " if r.success else "FAIL"
        log.info(f"  [{status}] {r.app}")
        for note in r.notes:
            log.info(f"         {note}")

    if all_manual:
        log.info(f"\n{'='*56}")
        log.info(f"  MANUAL STEPS REQUIRED ({len(all_manual)} total)")
        log.info(f"{'='*56}")
        for i, (app, step) in enumerate(all_manual, 1):
            log.info(f"\n  [{i}] [{app.upper()}]")
            for line in step.split("\n"):
                log.info(f"       {line}")

    log.info(f"\n{'='*56}")
    log.info(
        f"  OVERALL: {'SUCCESS' if failed == 0 else 'PARTIAL — see manual steps above'}"
    )
    log.info(f"{'='*56}\n")


# ---------------------------------------------------------------------------
# CLI argument parsing
# ---------------------------------------------------------------------------


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "TradiesVoice GHL Pipeline Runner\n"
            "Deploys TradiesVoice apps to a GHL sub-account.\n"
            "Requires GHL_AGENCY_API_KEY environment variable."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Plans:
  website          App 1 only — base website ($297/mo)
  starter_bundle   Apps 1+2+3 — Website + MCTB + Voice Widget ($497/mo)
  growth_bundle    Apps 1+2+3+5 — + Review Generator ($597/mo)
  full_suite       All 6 apps ($697/mo)

Examples:
  # Deploy full suite from onboarding JSON
  python ghl_pipeline_runner.py --client-data client_onboarding.json --plan full_suite

  # Deploy specific apps
  python ghl_pipeline_runner.py --client-data client_onboarding.json --apps mctb,review_generator

  # Deploy MCTB only (inline — requires env vars already set)
  python ghl_pipeline_runner.py \\
    --sub-account-id abc123 \\
    --business-name "Precision Plumbing" \\
    --owner-phone "+61412345678" \\
    --booking-link "https://book.precisionplumbing.com.au" \\
    --apps mctb
        """,
    )

    # Client data source — either a JSON file or inline args
    source_group = parser.add_mutually_exclusive_group()
    source_group.add_argument(
        "--client-data",
        metavar="PATH",
        help="Path to client onboarding JSON file",
    )

    # Inline client data (for quick single-module deploys)
    parser.add_argument("--sub-account-id", help="GHL sub-account/location ID")
    parser.add_argument("--business-name", help="Client business trading name")
    parser.add_argument("--owner-phone", help="Owner phone in E.164 format (+61XXXXXXXXX)")
    parser.add_argument("--booking-link", help="Online booking URL")
    parser.add_argument("--google-review-link", help="Google Business Profile review URL")
    parser.add_argument("--rating-page-url", help="Published GHL funnel URL for rating page")

    # What to deploy
    deploy_group = parser.add_mutually_exclusive_group(required=True)
    deploy_group.add_argument(
        "--plan",
        choices=list(PLANS.keys()),
        help="Pre-defined deployment plan",
    )
    deploy_group.add_argument(
        "--apps",
        help="Comma-separated list of apps to deploy (e.g. mctb,review_generator)",
    )

    return parser.parse_args()


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------


def main() -> None:
    args = parse_args()

    # Build client data dict
    if args.client_data:
        client = load_client_data(args.client_data)
    else:
        # Inline args — require at minimum the base fields
        if not all([args.sub_account_id, args.business_name, args.owner_phone, args.booking_link]):
            log.error(
                "When not using --client-data, the following are required:\n"
                "  --sub-account-id, --business-name, --owner-phone, --booking-link"
            )
            sys.exit(1)
        client = {
            "sub_account_id": args.sub_account_id,
            "business_name": args.business_name,
            "owner_phone": args.owner_phone,
            "booking_link": args.booking_link,
        }
        if args.google_review_link:
            client["google_review_link"] = args.google_review_link
        if args.rating_page_url:
            client["rating_page_url"] = args.rating_page_url

    # Resolve app list
    if args.plan:
        apps = PLANS[args.plan]
        log.info(f"Using plan: {args.plan} ({len(apps)} modules)")
    else:
        apps = [a.strip() for a in args.apps.split(",") if a.strip()]
        invalid = [a for a in apps if a not in VALID_APPS]
        if invalid:
            log.error(
                f"Invalid app names: {invalid}\n"
                f"Valid apps: {sorted(VALID_APPS)}"
            )
            sys.exit(1)

    # Run
    results = run_pipeline(apps, client)
    print_summary(results, client)

    # Exit code: 0 if all succeeded, 1 if any failed
    if any(not r.success for r in results):
        sys.exit(1)


if __name__ == "__main__":
    main()
