"""
webhook_bridge.py — FastAPI HTTP bridge for the stlc-agents webhook orchestrator.

Receives webhook POST requests from Azure DevOps or Jira Cloud and routes them
to the correct agent pipeline via the orchestrator.

Endpoints
─────────
  POST /webhook/ado   — Azure DevOps Service Hook (work item updated)
  POST /webhook/jira  — Jira Cloud webhook (issue updated)
  GET  /health        — Liveness check

Configuration (env vars or .env)
─────────────────────────────────
  # Required: ADO
  ADO_ORGANIZATION_URL      https://dev.azure.com/yourorg
  ADO_PROJECT_NAME          YourProject

  # Required: Jira
  JIRA_CLOUD_ID             your-cloud-id

  # LLM provider (default: anthropic)
  LLM_PROVIDER              anthropic|openai|copilot|ollama|grok|deepseek
  LLM_MODEL                 (optional) provider-specific model name
  LLM_API_KEY               API key (or use provider-specific env var)
  LLM_BASE_URL              (optional) base URL for ollama / deepseek

  # Helix-QA
  HELIX_PROJECT_ROOT        /path/to/helix-qa-project  (leave blank to skip Agent 4)

  # Webhook security
  ADO_WEBHOOK_SECRET        optional shared secret for ADO service hooks
  JIRA_WEBHOOK_SECRET       optional shared secret for Jira webhooks

  # Optional Agent 3 overrides
  HEALING_STRATEGY          role-label-text-ai | role-text-ai | ai-first
  AUTH_HOOK                 microsoft-sso | azure-keyvault-mfa | none
  ENABLE_VISUAL_REGRESSION  true | false
  ENABLE_TIMING_HEALING     true | false

Run:
  uvicorn stlc_agents.webhook_orchestrator.webhook_bridge:app --port 8080 --reload
"""
from __future__ import annotations

import hashlib
import hmac
import json
import logging
import os
from typing import Any

from dotenv import load_dotenv
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request, status
from fastapi.responses import JSONResponse

from stlc_agents.webhook_orchestrator.orchestrator import (
    Platform,
    WebhookEvent,
    run_pipeline,
)

load_dotenv()

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("stlc.bridge")

app = FastAPI(title="STLC Agents Webhook Bridge", version="1.0.0")


# ──────────────────────────────────────────────────────────────────────────────
# Config helpers
# ──────────────────────────────────────────────────────────────────────────────

def _env(key: str, default: str = "") -> str:
    return os.environ.get(key, default).strip()


def _bool_env(key: str, default: bool = True) -> bool:
    v = os.environ.get(key, "").strip().lower()
    if not v:
        return default
    return v not in ("false", "0", "no", "off")


def _llm_kwargs() -> dict:
    return {
        "llm_provider": _env("LLM_PROVIDER", "anthropic"),
        "llm_model":    _env("LLM_MODEL"),
        "llm_api_key":  _env("LLM_API_KEY"),
        "llm_base_url": _env("LLM_BASE_URL"),
    }


def _agent3_extra() -> dict:
    return {
        "healing_strategy":         _env("HEALING_STRATEGY", "role-label-text-ai"),
        "auth_hook":                _env("AUTH_HOOK", "microsoft-sso"),
        "enable_visual_regression": _bool_env("ENABLE_VISUAL_REGRESSION", True),
        "enable_timing_healing":    _bool_env("ENABLE_TIMING_HEALING", True),
    }


# ──────────────────────────────────────────────────────────────────────────────
# Webhook signature verification
# ──────────────────────────────────────────────────────────────────────────────

def _verify_hmac(
    body: bytes, signature_header: str | None, secret: str, prefix: str = "sha256="
) -> bool:
    if not secret:
        return True
    if not signature_header:
        return False
    expected = prefix + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)


# ──────────────────────────────────────────────────────────────────────────────
# ADO payload parser
# ──────────────────────────────────────────────────────────────────────────────

def _parse_ado_webhook(payload: dict) -> WebhookEvent | None:
    event_type = payload.get("eventType", "")
    if event_type not in (
        "workitem.updated",
        "workitem.created",
        "workitem.commented",
        "ms.vss-work.workitem-updated-event",
    ):
        logger.debug("ADO event type '%s' not handled", event_type)
        return None

    resource = payload.get("resource") or {}
    fields: dict[str, Any] = resource.get("fields", {})
    revision_fields: dict[str, Any] = (resource.get("revision") or {}).get("fields", {})

    # Extract state; for updates the field is a dict with oldValue/newValue
    previous_state = ""
    state_field = fields.get("System.State") or {}
    if isinstance(state_field, dict):
        old_val = (state_field.get("oldValue") or "").strip()
        new_val = (state_field.get("newValue") or state_field.get("value") or "").strip()
        # Skip no-op transitions — avoids reprocessing when only other fields changed
        if old_val and new_val and old_val.lower() == new_val.lower():
            logger.debug("ADO webhook: state unchanged (%s) — skipping", new_val)
            return None
        state = new_val
        previous_state = old_val
    else:
        state = str(state_field).strip()

    if not state:
        state = str(revision_fields.get("System.State", "")).strip()

    if not state:
        logger.debug("ADO webhook: could not determine state")
        return None

    wi_id = str(resource.get("workItemId") or resource.get("id", ""))
    if not wi_id:
        logger.debug("ADO webhook: no work item ID")
        return None

    wi_type_field = fields.get("System.WorkItemType") or {}
    if isinstance(wi_type_field, dict):
        wi_type = wi_type_field.get("newValue") or wi_type_field.get("value") or ""
    else:
        wi_type = str(wi_type_field)
    if not wi_type:
        wi_type = revision_fields.get("System.WorkItemType", "")

    title = revision_fields.get("System.Title", "")

    # Extract org URL and project from payload first; fall back to env vars.
    # ADO service hooks include these in resourceContainers.
    resource_containers = payload.get("resourceContainers") or {}
    org_url = (
        (resource_containers.get("account") or {}).get("baseUrl", "").rstrip("/")
        or _env("ADO_ORGANIZATION_URL")
    )
    project_name = (
        (resource_containers.get("project") or {}).get("name", "")
        or _env("ADO_PROJECT_NAME")
    )

    return WebhookEvent(
        platform=Platform.ADO,
        work_item_id=wi_id,
        state=state,
        previous_state=previous_state,
        work_item_type=wi_type,
        title=title,
        organization_url=org_url,
        project_name=project_name,
        helix_project_root=_env("HELIX_PROJECT_ROOT"),
        extra=_agent3_extra(),
        **_llm_kwargs(),
    )


# ──────────────────────────────────────────────────────────────────────────────
# Jira payload parser
# ──────────────────────────────────────────────────────────────────────────────

def _parse_jira_webhook(payload: dict) -> WebhookEvent | None:
    event_type = payload.get("webhookEvent", "")
    if event_type not in ("jira:issue_updated", "jira:issue_created"):
        logger.debug("Jira event type '%s' not handled", event_type)
        return None

    issue = payload.get("issue", {})
    if not issue:
        logger.debug("Jira webhook: no issue in payload")
        return None

    issue_key = issue.get("key", "")
    if not issue_key:
        logger.debug("Jira webhook: no issue key")
        return None

    fields = issue.get("fields", {})

    state = (fields.get("status") or {}).get("name", "")
    if not state:
        logger.debug("Jira webhook: could not determine status")
        return None

    # For updates, require an actual status transition in the changelog.
    # This prevents re-running pipelines when only comments or other fields changed.
    previous_state = ""
    if event_type == "jira:issue_updated":
        changelog = payload.get("changelog") or {}
        status_item: dict | None = None
        for item in changelog.get("items", []):
            if item.get("field") == "status":
                status_item = item
                break
        if status_item is None:
            logger.debug("Jira webhook: no status field in changelog — skipping")
            return None
        from_str = (status_item.get("fromString") or "").strip()
        to_str   = (status_item.get("toString")   or "").strip()
        if from_str.lower() == to_str.lower():
            logger.debug("Jira webhook: status unchanged (%s) — skipping", to_str)
            return None
        previous_state = from_str

    wi_type  = (fields.get("issuetype") or {}).get("name", "")
    title    = fields.get("summary", "")
    proj_key = (fields.get("project") or {}).get("key", "")
    # Derive project key from issue key when not present in fields (e.g. minimal payloads)
    if not proj_key and "-" in issue_key:
        proj_key = issue_key.split("-")[0]

    return WebhookEvent(
        platform=Platform.JIRA,
        work_item_id=issue_key,
        state=state,
        previous_state=previous_state,
        work_item_type=wi_type,
        title=title,
        cloud_id=_env("JIRA_CLOUD_ID"),
        project_key=proj_key,
        helix_project_root=_env("HELIX_PROJECT_ROOT"),
        extra=_agent3_extra(),
        **_llm_kwargs(),
    )


# ──────────────────────────────────────────────────────────────────────────────
# Routes
# ──────────────────────────────────────────────────────────────────────────────

async def _pipeline_bg(event: WebhookEvent) -> None:
    try:
        result = await run_pipeline(event)
        if result.success:
            logger.info("Pipeline done: wi=%s steps=%s", event.work_item_id, result.steps)
        else:
            logger.error(
                "Pipeline failed: wi=%s error=%s steps=%s",
                event.work_item_id, result.error, result.steps,
            )
    except Exception:
        logger.exception("Unhandled pipeline error for wi=%s", event.work_item_id)


@app.get("/health")
async def health() -> dict:
    return {"status": "ok", "service": "stlc-webhook-bridge"}


@app.post("/webhook/ado")
async def webhook_ado(request: Request, background_tasks: BackgroundTasks) -> JSONResponse:
    body = await request.body()
    sig  = request.headers.get("X-Hub-Signature-256") or request.headers.get("X-ADO-Signature")
    if not _verify_hmac(body, sig, _env("ADO_WEBHOOK_SECRET")):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid signature")

    try:
        payload = json.loads(body)
    except json.JSONDecodeError:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid JSON")

    event = _parse_ado_webhook(payload)
    if event is None:
        return JSONResponse({"accepted": False, "reason": "event not actionable"})

    missing = []
    if not event.organization_url:
        missing.append("ADO_ORGANIZATION_URL")
    if not event.project_name:
        missing.append("ADO_PROJECT_NAME")
    if missing:
        msg = f"Missing required env var(s): {', '.join(missing)}. Set them in .env and restart."
        logger.error("ADO webhook config error: %s", msg)
        return JSONResponse(
            {"accepted": False, "error": msg},
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        )

    logger.info(
        "ADO webhook: work_item=%s state=%s type=%s",
        event.work_item_id, event.state, event.work_item_type,
    )

    # Acknowledge immediately — ADO retries on 5xx or slow responses.
    # The pipeline runs in the background; results are logged.
    background_tasks.add_task(_pipeline_bg, event)
    return JSONResponse({"accepted": True, "work_item": event.work_item_id, "stage": "queued"})


@app.post("/webhook/jira")
async def webhook_jira(request: Request, background_tasks: BackgroundTasks) -> JSONResponse:
    body = await request.body()
    sig  = request.headers.get("X-Hub-Signature-256") or request.headers.get("X-Jira-Signature")
    if not _verify_hmac(body, sig, _env("JIRA_WEBHOOK_SECRET")):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid signature")

    try:
        payload = json.loads(body)
    except json.JSONDecodeError:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid JSON")

    event = _parse_jira_webhook(payload)
    if event is None:
        return JSONResponse({"accepted": False, "reason": "event not actionable"})

    if not event.cloud_id:
        msg = "Missing required env var: JIRA_CLOUD_ID. Set it in .env and restart."
        logger.error("Jira webhook config error: %s", msg)
        return JSONResponse(
            {"accepted": False, "error": msg},
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        )

    logger.info(
        "Jira webhook: issue=%s state=%s type=%s",
        event.work_item_id, event.state, event.work_item_type,
    )

    # Acknowledge immediately — Jira retries on 5xx or slow responses.
    background_tasks.add_task(_pipeline_bg, event)
    return JSONResponse({"accepted": True, "issue": event.work_item_id, "stage": "queued"})
