"""
Phase 50: External Service Integration Hub.

Unified framework for connecting Brynq to external services — GitHub, Slack,
Discord, Jira, Linear, Notion, and arbitrary webhooks/APIs. Provides
registration, lifecycle management, bidirectional event dispatch, pre-built
templates, connection testing, and stats.

Credentials are NEVER stored — all secrets come from environment variables
(BYOK model). Config holds service-specific settings like channel IDs, API
URLs, and project keys.

Storage:
  state/broker/integrations/
    {name}.json          — per-integration config + metadata
    events.jsonl         — append-only event log (sent + received)
"""

import json
import logging
import os
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
from urllib.request import Request, urlopen
from urllib.error import URLError

from brynq.server import (
    BROKER_DIR,
    _write_json_sync,
    _read_jsonl_sync,
    _append_jsonl_sync,
)

logger = logging.getLogger(__name__)

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

INTEGRATIONS_DIR = BROKER_DIR / "integrations"
EVENTS_FILE = INTEGRATIONS_DIR / "events.jsonl"

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

# ── Constants ──────────────────────────────────────────────────────────────

VALID_SERVICE_TYPES = [
    "github",
    "slack",
    "discord",
    "jira",
    "linear",
    "notion",
    "custom_webhook",
    "custom_api",
]

VALID_STATUSES = ["active", "paused", "error"]

# ── In-process handler registry (not persisted) ──────────────────────────

# { (integration_name, event_type): [handler_fn, ...] }
_incoming_handlers: Dict[Tuple[str, str], List[Callable]] = {}


def _reset_handlers() -> None:
    """Clear all in-memory handlers. Used by tests."""
    _incoming_handlers.clear()


# ── Helpers ────────────────────────────────────────────────────────────────

def _integration_path(name: str) -> Path:
    return INTEGRATIONS_DIR / f"{name}.json"


def _load_integration(name: str) -> Optional[dict]:
    path = _integration_path(name)
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


def _save_integration(integration: dict) -> None:
    _write_json_sync(_integration_path(integration["name"]), integration)


def _log_event(integration_name: str, direction: str, event_type: str,
               payload: dict, success: bool, detail: str = "") -> dict:
    """Append an event record to the shared events log."""
    record = {
        "event_id": uuid.uuid4().hex[:12],
        "integration": integration_name,
        "direction": direction,  # "outbound" or "inbound"
        "event_type": event_type,
        "payload_summary": str(payload)[:200],
        "success": success,
        "detail": detail,
        "timestamp": datetime.now(timezone.utc).isoformat(),
    }
    _append_jsonl_sync(EVENTS_FILE, record)
    return record


# ── Register Integration ──────────────────────────────────────────────────


def register_integration(
    name: str,
    service_type: str,
    config: dict,
    description: str = "",
) -> dict:
    """Register an external service integration.

    Args:
        name: Unique identifier for this integration (e.g. "prod-slack").
        service_type: One of VALID_SERVICE_TYPES.
        config: Service-specific settings (URLs, channel IDs, project keys).
                Credentials must NOT be stored here — use env vars.
        description: Human-readable description.

    Returns:
        The integration record, or dict with "error" key on failure.
    """
    if not name or not name.strip():
        return {"error": "Name is required"}

    if service_type not in VALID_SERVICE_TYPES:
        return {"error": f"Invalid service_type '{service_type}'. "
                         f"Must be one of: {', '.join(VALID_SERVICE_TYPES)}"}

    if _load_integration(name) is not None:
        return {"error": f"Integration '{name}' already exists"}

    integration = {
        "name": name,
        "service_type": service_type,
        "config": config,
        "description": description or f"{service_type} integration",
        "status": "active",
        "created_at": datetime.now(timezone.utc).isoformat(),
        "updated_at": datetime.now(timezone.utc).isoformat(),
        "stats": {
            "events_sent": 0,
            "events_received": 0,
            "errors": 0,
            "last_event_at": None,
            "uptime_since": datetime.now(timezone.utc).isoformat(),
        },
    }

    _save_integration(integration)
    return integration


# ── List / Get ─────────────────────────────────────────────────────────────


def list_integrations(
    service_type: Optional[str] = None,
    status: Optional[str] = None,
) -> list:
    """List integrations, optionally filtered by service_type and/or status."""
    results = []
    for f in sorted(INTEGRATIONS_DIR.glob("*.json")):
        try:
            rec = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if service_type and rec.get("service_type") != service_type:
            continue
        if status and rec.get("status") != status:
            continue
        results.append(rec)
    return results


def get_integration(name: str) -> Optional[dict]:
    """Get a single integration by name. Returns None if not found."""
    return _load_integration(name)


# ── Enable / Disable / Remove ─────────────────────────────────────────────


def enable_integration(name: str) -> dict:
    """Set integration status to active."""
    rec = _load_integration(name)
    if not rec:
        return {"error": f"Integration '{name}' not found"}
    rec["status"] = "active"
    rec["updated_at"] = datetime.now(timezone.utc).isoformat()
    rec["stats"]["uptime_since"] = datetime.now(timezone.utc).isoformat()
    _save_integration(rec)
    return rec


def disable_integration(name: str) -> dict:
    """Set integration status to paused."""
    rec = _load_integration(name)
    if not rec:
        return {"error": f"Integration '{name}' not found"}
    rec["status"] = "paused"
    rec["updated_at"] = datetime.now(timezone.utc).isoformat()
    _save_integration(rec)
    return rec


def remove_integration(name: str) -> dict:
    """Permanently delete an integration."""
    path = _integration_path(name)
    if not path.exists():
        return {"error": f"Integration '{name}' not found"}
    path.unlink(missing_ok=True)
    # Clean up any handlers
    keys_to_remove = [k for k in _incoming_handlers if k[0] == name]
    for k in keys_to_remove:
        del _incoming_handlers[k]
    return {"removed": name}


# ── Send Event (outbound) ─────────────────────────────────────────────────


def _do_http_post(url: str, payload: dict, headers: Optional[dict] = None,
                  timeout: int = 10) -> Tuple[bool, str]:
    """Perform an HTTP POST. Returns (success, body_or_error)."""
    try:
        data = json.dumps(payload).encode("utf-8")
        req_headers = {"Content-Type": "application/json"}
        if headers:
            req_headers.update(headers)
        req = Request(url, data=data, headers=req_headers, method="POST")
        with urlopen(req, timeout=timeout) as resp:
            body = resp.read().decode("utf-8", errors="replace")
            return (True, body)
    except Exception as exc:
        return (False, str(exc))


def _format_payload(service_type: str, event_type: str, payload: dict) -> dict:
    """Format a payload for the target service type."""
    if service_type == "slack":
        text = payload.get("text") or payload.get("message") or json.dumps(payload)
        return {"text": f"[{event_type}] {text}"}
    elif service_type == "discord":
        text = payload.get("text") or payload.get("message") or json.dumps(payload)
        return {"content": f"[{event_type}] {text}"}
    elif service_type == "github":
        return {
            "event_type": event_type,
            "client_payload": payload,
        }
    elif service_type == "jira":
        return {
            "fields": {
                "summary": payload.get("title", event_type),
                "description": payload.get("description", json.dumps(payload)),
            }
        }
    elif service_type == "linear":
        return {
            "title": payload.get("title", event_type),
            "description": payload.get("description", json.dumps(payload)),
        }
    elif service_type == "notion":
        return {
            "parent": payload.get("parent", {}),
            "properties": {
                "title": {"title": [{"text": {"content": payload.get("title", event_type)}}]},
            },
        }
    else:
        # custom_webhook, custom_api, or unknown — pass through
        return {"event_type": event_type, "payload": payload}


def send_to_integration(
    name: str,
    event_type: str,
    payload: dict,
) -> Tuple[bool, str]:
    """Dispatch an event to the configured external service.

    Returns:
        (success, response_or_error) tuple.
    """
    rec = _load_integration(name)
    if not rec:
        return (False, f"Integration '{name}' not found")

    if rec["status"] != "active":
        return (False, f"Integration '{name}' is {rec['status']}, not active")

    config = rec.get("config", {})
    service_type = rec["service_type"]

    # Determine target URL
    url = config.get("webhook_url") or config.get("api_url") or config.get("url")
    if not url:
        # Mark error
        rec["status"] = "error"
        rec["stats"]["errors"] += 1
        _save_integration(rec)
        _log_event(name, "outbound", event_type, payload, False, "No URL configured")
        return (False, "No URL configured in integration config")

    formatted = _format_payload(service_type, event_type, payload)

    # Add auth headers from env
    headers = dict(config.get("headers", {}))
    env_token_key = config.get("auth_env_var")
    if env_token_key:
        token = os.environ.get(env_token_key, "")
        if token:
            headers["Authorization"] = f"Bearer {token}"

    success, response = _do_http_post(url, formatted, headers=headers)

    # Update stats
    rec["stats"]["events_sent"] += 1
    rec["stats"]["last_event_at"] = datetime.now(timezone.utc).isoformat()
    if not success:
        rec["stats"]["errors"] += 1
    _save_integration(rec)

    _log_event(name, "outbound", event_type, payload, success, response[:200] if response else "")
    return (success, response)


# ── Receive Events (inbound) ──────────────────────────────────────────────


def register_incoming_handler(
    integration_name: str,
    event_type: str,
    handler_fn: Callable,
) -> str:
    """Register a callback for incoming events from an integration.

    Args:
        integration_name: Which integration the events come from.
        event_type: The event type to handle (e.g. "issue.created").
        handler_fn: Callable that receives (event_type, payload) -> Any.

    Returns:
        A handler ID string.
    """
    key = (integration_name, event_type)
    if key not in _incoming_handlers:
        _incoming_handlers[key] = []
    _incoming_handlers[key].append(handler_fn)
    return f"handler-{uuid.uuid4().hex[:8]}"


def process_incoming(
    integration_name: str,
    event_type: str,
    payload: dict,
) -> dict:
    """Process an incoming event from an external service.

    Runs all matching handlers and updates stats.

    Returns:
        Summary dict with handler_count, successes, errors.
    """
    rec = _load_integration(integration_name)
    if not rec:
        return {"error": f"Integration '{integration_name}' not found"}

    if rec["status"] != "active":
        return {"error": f"Integration '{integration_name}' is {rec['status']}, not active"}

    key = (integration_name, event_type)
    handlers = _incoming_handlers.get(key, [])

    successes = 0
    errors_list = []
    for fn in handlers:
        try:
            fn(event_type, payload)
            successes += 1
        except Exception as exc:
            errors_list.append(str(exc))

    # Update stats
    rec["stats"]["events_received"] += 1
    rec["stats"]["last_event_at"] = datetime.now(timezone.utc).isoformat()
    if errors_list:
        rec["stats"]["errors"] += len(errors_list)
    _save_integration(rec)

    _log_event(integration_name, "inbound", event_type, payload,
               success=len(errors_list) == 0,
               detail=f"{successes} ok, {len(errors_list)} errors")

    return {
        "handler_count": len(handlers),
        "successes": successes,
        "errors": errors_list,
    }


# ── Integration Templates ────────────────────────────────────────────────


def github_template() -> dict:
    """Pre-built template for GitHub integration.

    Mappings:
      - issue.created  -> create task in broker
      - pull_request.merged -> complete task in broker
    """
    return {
        "service_type": "github",
        "description": "GitHub integration — issues to tasks, PRs to completions",
        "config": {
            "api_url": "https://api.github.com",
            "auth_env_var": "GITHUB_TOKEN",
            "headers": {"Accept": "application/vnd.github+json"},
        },
        "event_mappings": {
            "issue.created": {
                "action": "create_task",
                "field_map": {
                    "title": "$.title",
                    "description": "$.body",
                    "source": "github",
                },
            },
            "pull_request.merged": {
                "action": "complete_task",
                "field_map": {
                    "ref": "$.head.ref",
                    "source": "github",
                },
            },
        },
    }


def slack_template() -> dict:
    """Pre-built template for Slack integration.

    Mappings:
      - message.posted -> broker post
      - task.completed -> Slack notification
    """
    return {
        "service_type": "slack",
        "description": "Slack integration — messages to broker, task updates to Slack",
        "config": {
            "webhook_url": "",  # User fills in
            "auth_env_var": "SLACK_BOT_TOKEN",
            "channel": "#general",
        },
        "event_mappings": {
            "message.posted": {
                "action": "broker_post",
                "field_map": {
                    "text": "$.text",
                    "channel": "$.channel",
                    "source": "slack",
                },
            },
            "task.completed": {
                "action": "slack_notify",
                "field_map": {
                    "text": "Task completed: $.task_id",
                    "channel": "#general",
                },
            },
        },
    }


def discord_template() -> dict:
    """Pre-built template for Discord integration.

    Mappings:
      - message.posted -> broker post
      - task.completed -> Discord notification
    """
    return {
        "service_type": "discord",
        "description": "Discord integration — messages to broker, task updates to Discord",
        "config": {
            "webhook_url": "",  # User fills in
            "auth_env_var": "DISCORD_BOT_TOKEN",
            "channel_id": "",
        },
        "event_mappings": {
            "message.posted": {
                "action": "broker_post",
                "field_map": {
                    "content": "$.content",
                    "channel_id": "$.channel_id",
                    "source": "discord",
                },
            },
            "task.completed": {
                "action": "discord_notify",
                "field_map": {
                    "content": "Task completed: $.task_id",
                },
            },
        },
    }


def get_template(service_type: str) -> Optional[dict]:
    """Return the pre-built template for a service type, or None."""
    templates = {
        "github": github_template,
        "slack": slack_template,
        "discord": discord_template,
    }
    fn = templates.get(service_type)
    return fn() if fn else None


# ── Integration Stats ────────────────────────────────────────────────────


def get_integration_stats(name: str) -> dict:
    """Return stats for a single integration.

    Returns:
        Dict with events_sent, events_received, errors, last_event_at, uptime.
    """
    rec = _load_integration(name)
    if not rec:
        return {"error": f"Integration '{name}' not found"}

    stats = rec.get("stats", {})
    uptime_since = stats.get("uptime_since")
    uptime_seconds = 0.0
    if uptime_since and rec["status"] == "active":
        try:
            start = datetime.fromisoformat(uptime_since)
            uptime_seconds = (datetime.now(timezone.utc) - start).total_seconds()
        except (ValueError, TypeError):
            pass

    return {
        "name": name,
        "status": rec["status"],
        "service_type": rec["service_type"],
        "events_sent": stats.get("events_sent", 0),
        "events_received": stats.get("events_received", 0),
        "errors": stats.get("errors", 0),
        "last_event_at": stats.get("last_event_at"),
        "uptime_seconds": round(uptime_seconds, 1),
    }


def get_all_stats() -> dict:
    """Return platform-wide integration health summary."""
    integrations = list_integrations()
    total_sent = 0
    total_received = 0
    total_errors = 0
    active_count = 0
    paused_count = 0
    error_count = 0

    per_integration = []
    for rec in integrations:
        stats = rec.get("stats", {})
        total_sent += stats.get("events_sent", 0)
        total_received += stats.get("events_received", 0)
        total_errors += stats.get("errors", 0)
        status = rec.get("status", "active")
        if status == "active":
            active_count += 1
        elif status == "paused":
            paused_count += 1
        elif status == "error":
            error_count += 1
        per_integration.append({
            "name": rec["name"],
            "service_type": rec["service_type"],
            "status": status,
            "events_sent": stats.get("events_sent", 0),
            "events_received": stats.get("events_received", 0),
            "errors": stats.get("errors", 0),
        })

    return {
        "total_integrations": len(integrations),
        "active": active_count,
        "paused": paused_count,
        "error": error_count,
        "total_events_sent": total_sent,
        "total_events_received": total_received,
        "total_errors": total_errors,
        "integrations": per_integration,
    }


# ── Connection Test ──────────────────────────────────────────────────────


def test_integration(name: str) -> dict:
    """Send a test ping to the integration and report results.

    Returns:
        Dict with success, latency_ms, and detail.
    """
    rec = _load_integration(name)
    if not rec:
        return {"error": f"Integration '{name}' not found"}

    config = rec.get("config", {})
    url = config.get("webhook_url") or config.get("api_url") or config.get("url")
    if not url:
        return {"success": False, "latency_ms": 0, "detail": "No URL configured"}

    headers = dict(config.get("headers", {}))
    env_token_key = config.get("auth_env_var")
    if env_token_key:
        token = os.environ.get(env_token_key, "")
        if token:
            headers["Authorization"] = f"Bearer {token}"

    ping_payload = {
        "type": "integration_test",
        "integration": name,
        "service_type": rec["service_type"],
        "timestamp": datetime.now(timezone.utc).isoformat(),
    }

    start = time.monotonic()
    success, response = _do_http_post(url, ping_payload, headers=headers, timeout=10)
    latency_ms = round((time.monotonic() - start) * 1000, 1)

    return {
        "success": success,
        "latency_ms": latency_ms,
        "detail": response[:300] if response else "",
    }
