"""
HTTP surface for the flow engine.

  POST /flow/webhook              ← Picky Assist webhook (parallel to /mexico/get_response)
  GET  /flow/sim                  ← HTML phone-mockup simulator
  POST /flow/sim/send             ← simulator's JSON endpoint
  POST /flow/sim/reset            ← reset simulator state

  GET  /flow/builder/             ← flow list / picker landing
  GET  /flow/builder/<flow_id>    ← visual canvas editor
  GET  /flow/api/flows            ← list flows
  POST /flow/api/flows            ← create new flow
  GET  /flow/api/flows/<flow_id>  ← read flow JSON
  PUT  /flow/api/flows/<flow_id>  ← save flow JSON
"""
from __future__ import annotations

import json
import logging

from django.conf import settings
from django.contrib import auth as dj_auth
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, JsonResponse, HttpRequest, Http404
from django.shortcuts import render, redirect
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET, require_POST, require_http_methods

from .models import AppSetting, Flow
from .picky_adapter import parse_inbound, send_action
from .runner import run_turn, load_state, FlowStateModel

logger = logging.getLogger(__name__)

DEFAULT_FLOW_ID = getattr(settings, "FLOW_DEFAULT_ID", "demo")


@csrf_exempt
@require_POST
def flow_webhook(request: HttpRequest) -> HttpResponse:
    import json as _json
    import requests as _requests

    inbound = parse_inbound(request.body)
    number = inbound.get("number")
    if not number:
        return HttpResponse("missing number", status=400)

    from .engine import FlowState as _FlowState, match_trigger, step
    from .runner import load_state, save_state

    # Routing priority:
    # 1. If the inbound matches a trigger in any active flow, START that flow
    #    (re-trigger always wins — matches the legacy "Hola"/"CONTINUAR" reset).
    # 2. Else if the user has existing state mid-flow, continue that flow.
    # 3. Else fall back (or no-op).
    flow_id = None
    flow = None
    triggered_fresh = False

    # Only LIVE flows are eligible for new-trigger matching. Drafts/archived
    # are testable via the simulator but won't intercept real WhatsApp traffic.
    # allow_default=False — the default trigger is a catch-all for stateless
    # users and would otherwise reset state on every message.
    for row in Flow.objects.filter(status=Flow.STATUS_LIVE).order_by("flow_id"):
        candidate = _json.loads(row.definition)
        if match_trigger(candidate, inbound, allow_default=False) is not None:
            flow_id = row.flow_id
            flow = candidate
            triggered_fresh = True
            break

    if flow is None:
        existing_states = list(
            FlowStateModel.objects.filter(mobile_number=number).order_by("-updated_at")
        )
        for st in existing_states:
            if st.current_node_id:
                try:
                    # Continue the user's flow regardless of draft/live —
                    # we don't want to strand someone mid-conversation just
                    # because the flow got demoted to draft.
                    row = Flow.objects.get(flow_id=st.flow_id)
                    if row.status == Flow.STATUS_ARCHIVED:
                        continue
                    flow_id = st.flow_id
                    flow = _json.loads(row.definition)
                    break
                except Flow.DoesNotExist:
                    continue

    if flow is None:
        flow_id = DEFAULT_FLOW_ID
        try:
            flow = _json.loads(Flow.objects.get(flow_id=flow_id).definition)
        except Flow.DoesNotExist:
            flow = {"triggers": [], "nodes": []}

    if triggered_fresh:
        # Reset state for this flow so the trigger walks from the entry node.
        # We preserve "sticky" profile fields the user already provided so they
        # don't have to re-enter them every time they type "hola". Sticky list
        # is conservative: only things we'd otherwise prompt for again (CURP,
        # captured CURP-data from onboarding QR, etc.).
        loaded = load_state(number, flow_id)
        STICKY_KEYS = {"manual_curp", "curp_data", "etnia"}
        sticky_ctx = {k: v for k, v in (loaded.context or {}).items()
                      if k in STICKY_KEYS}
        prev_state = _FlowState(current_node_id=None, context=sticky_ctx)
    else:
        prev_state = load_state(number, flow_id)
    user_was_in_flow = prev_state.current_node_id is not None

    # Log inbound BEFORE step() — captures what the user sent regardless of
    # whether the engine matches a trigger / advances state / errors out.
    try:
        from . import event_log as _evlog
        _evlog.log_inbound(
            mobile_number=number, flow_id=flow_id,
            inbound=inbound, prev_node_id=prev_state.current_node_id,
            context=prev_state.context,
        )
    except Exception:
        logger.exception("event log inbound failed")

    new_state, actions = step(flow, prev_state, inbound)

    not_our_user = (
        not user_was_in_flow
        and new_state.current_node_id is None
        and not actions
    )

    fallback_url = (getattr(settings, "FALLBACK_WEBHOOK_URL", "") or "").strip()

    if not_our_user and fallback_url:
        # Forward inbound payload verbatim to legacy endpoint — production
        # behavior is preserved for users not engaging our new flow.
        try:
            r = _requests.post(
                fallback_url,
                data=request.body,
                headers={"Content-Type": request.content_type or "application/json"},
                timeout=20,
            )
            return HttpResponse(
                r.content,
                status=r.status_code,
                content_type=r.headers.get("Content-Type", "application/json"),
            )
        except Exception as e:
            logger.exception("fallback forward failed")
            return JsonResponse(
                {"ok": False, "fallback_error": str(e), "fallback_url": fallback_url},
                status=502,
            )

    # Save state EARLY — before any Picky calls. Picky network round-trips
    # take seconds; if a second webhook for the same user arrives in that
    # window (e.g. fast button tap), it must see the user's progressed state,
    # not the stale prior state. Saving here shrinks the race window to a
    # single DB write (~ms).
    save_state(number, flow_id, new_state)

    results = []
    deliveries_attempted = 0
    deliveries_ok = 0
    import time as _time
    # Track whether we just shipped a send to Picky — used to insert an
    # implicit 1s gap between back-to-back sends so WhatsApp doesn't reorder
    # them on the recipient's side (media + menu being the most common
    # offender). Explicit `wait` actions reset this flag, so flow-author
    # waits aren't double-counted.
    last_was_send = False
    last_send_type = None
    INTER_SEND_PACING_TEXT = 1.0    # text → text/menu — short rendering
    INTER_SEND_PACING_MEDIA = 2.0   # media → anything — image upload takes longer
    for action in actions:
        # `wait` action: actually sleep so Picky/WhatsApp has time to deliver
        # the previous message in order before we queue the next. Capped at 5s
        # per wait so a misconfigured flow can't pin a worker forever.
        if action.type == "wait":
            try:
                secs = float(action.payload.get("seconds", 1))
                if 0 < secs <= 5:
                    _time.sleep(secs)
            except (TypeError, ValueError):
                pass
            results.append({"type": "wait", "result": {"ok": True, "slept": True}})
            last_was_send = False
            last_send_type = None
            continue

        # Implicit pacing between consecutive sends (media → text, text → list,
        # text → buttons, etc.). Longer gap after media so WhatsApp finishes
        # rendering the image before the next message arrives — otherwise
        # the menu can land *before* the QR image on the user's phone.
        if last_was_send:
            gap = (INTER_SEND_PACING_MEDIA
                   if last_send_type == "send_media"
                   else INTER_SEND_PACING_TEXT)
            _time.sleep(gap)

        result = send_action(number, action)
        results.append({"type": action.type, "result": result})
        last_was_send = True
        last_send_type = action.type

        # Best-effort: record what we shipped to Picky into the event log.
        try:
            _evlog.log_outbound(
                mobile_number=number, flow_id=flow_id,
                action=action, node_id=new_state.current_node_id,
                context=new_state.context,
            )
        except Exception:
            logger.exception("event log outbound failed")
        if result.get("skipped"):
            continue
        deliveries_attempted += 1
        # Picky Assist HTTP 200 isn't enough — its body wraps the real status.
        # 100=Success, 802=session expired, 402=empty list, 401=auth fail, etc.
        body_text = result.get("response_text") or ""
        body_status = None
        try:
            body_status = int(_json.loads(body_text).get("status", 0))
        except Exception:
            pass
        # Treat 100 (Picky's documented success) as the only success.
        if result.get("ok") and body_status == 100:
            deliveries_ok += 1

    # Auto-rollback policy: if there were deliveries to attempt and ALL of
    # them failed, the user didn't see anything — revert the state so they
    # aren't stranded mid-flow. Best-effort: only roll back if no other
    # webhook has changed the row since we saved it (compare-and-swap pattern
    # via select_for_update inside a transaction).
    rolled_back = False
    if deliveries_attempted > 0 and deliveries_ok == 0:
        logger.warning(
            "Rolling back state for %s @ %s: all %d outbound actions rejected by Picky Assist",
            number, flow_id, deliveries_attempted,
        )
        try:
            _evlog.log_sys(
                mobile_number=number, flow_id=flow_id,
                event_type="rollback",
                summary=f"all {deliveries_attempted} outbound actions rejected by Picky",
                node_id=new_state.current_node_id,
                context=new_state.context,
            )
        except Exception:
            logger.exception("event log rollback failed")
        from django.db import transaction
        try:
            with transaction.atomic():
                current = (FlowStateModel.objects
                           .select_for_update()
                           .get(mobile_number=number, flow_id=flow_id))
                # Only revert if the row still holds OUR write — otherwise a
                # newer webhook is in progress and rolling back would clobber it.
                if current.current_node_id == new_state.current_node_id:
                    if user_was_in_flow:
                        save_state(number, flow_id, prev_state)
                    else:
                        current.delete()
                    rolled_back = True
        except FlowStateModel.DoesNotExist:
            pass

    return JsonResponse({
        "ok": True,
        "current_node_id": new_state.current_node_id if not rolled_back else (prev_state.current_node_id),
        "actions_sent": len(actions),
        "deliveries_ok": deliveries_ok,
        "deliveries_attempted": deliveries_attempted,
        "rolled_back": rolled_back,
        "fallback_used": False,
        "results": results,
    })


@login_required
@require_GET
def simulator(request: HttpRequest) -> HttpResponse:
    return render(request, "flowbuilder/simulator.html", {
        "default_flow_id": DEFAULT_FLOW_ID,
    })


@csrf_exempt
@login_required
@require_POST
def simulator_send(request: HttpRequest) -> JsonResponse:
    """Browser sends {mobile, flow_id, message?, choice_id?} — we run a turn and return the actions."""
    body = json.loads(request.body or b"{}")
    mobile = str(body.get("mobile", "")).strip()
    flow_id = body.get("flow_id") or DEFAULT_FLOW_ID
    name = body.get("name") or "Tester"
    message = body.get("message")
    choice_id = body.get("choice_id")

    if not mobile:
        return JsonResponse({"ok": False, "error": "mobile required"}, status=400)

    inbound = {
        "number": mobile,
        "name": name,
        "message_in_raw": message,
        "interactive": {"id": choice_id} if choice_id else None,
        "latitude": body.get("latitude") or body.get("lat"),
        "longitude": body.get("longitude") or body.get("lng"),
    }

    new_state, actions = run_turn(flow_id, mobile, inbound)

    return JsonResponse({
        "ok": True,
        "current_node_id": new_state.current_node_id,
        "actions": [{"type": a.type, "payload": a.payload} for a in actions],
    })


@csrf_exempt
@login_required
@require_POST
def simulator_reset(request: HttpRequest) -> JsonResponse:
    body = json.loads(request.body or b"{}")
    mobile = str(body.get("mobile", "")).strip()
    flow_id = body.get("flow_id") or DEFAULT_FLOW_ID
    if not mobile:
        return JsonResponse({"ok": False, "error": "mobile required"}, status=400)

    deleted, _ = FlowStateModel.objects.filter(mobile_number=mobile, flow_id=flow_id).delete()
    return JsonResponse({"ok": True, "deleted": deleted})


# ---------------------------------------------------------------------------
# Builder UI (Botpress-style visual flow editor)
# ---------------------------------------------------------------------------


def _no_cache(response: HttpResponse) -> HttpResponse:
    response["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    response["Pragma"] = "no-cache"
    response["Expires"] = "0"
    return response


@login_required
@require_GET
def builder_index(request: HttpRequest) -> HttpResponse:
    flows = Flow.objects.all().order_by("-updated_at")
    return _no_cache(render(request, "flowbuilder/builder_index.html", {"flows": flows}))


@login_required
@require_GET
def dashboard_view(request: HttpRequest) -> HttpResponse:
    """Aggregate metrics from the FlowEvent table: how many users, what
    services they touched, recent activity. All read-only."""
    from .models import FlowEvent, FlowState as FSM
    from django.db.models import Count
    from django.utils import timezone
    from datetime import timedelta

    now = timezone.now()
    day_ago = now - timedelta(days=1)
    week_ago = now - timedelta(days=7)

    # Top-line cards
    total_users    = FSM.objects.values("mobile_number").distinct().count()
    users_24h      = (FlowEvent.objects
                      .filter(created_at__gte=day_ago)
                      .values("mobile_number").distinct().count())
    users_7d       = (FlowEvent.objects
                      .filter(created_at__gte=week_ago)
                      .values("mobile_number").distinct().count())
    msgs_24h_in    = FlowEvent.objects.filter(created_at__gte=day_ago, direction="in").count()
    msgs_24h_out   = FlowEvent.objects.filter(created_at__gte=day_ago, direction="out").count()

    # Service breakdown over the last 7 days (in-events = user actions, so
    # one "click on Generar" = one tap, not per-message). Drop empties.
    svc_counts = (FlowEvent.objects
                  .filter(created_at__gte=week_ago, direction="in")
                  .exclude(service="")
                  .values("service")
                  .annotate(n=Count("event_id"))
                  .order_by("-n"))
    svc_total = sum(s["n"] for s in svc_counts) or 1
    services = [{"name": s["service"], "n": s["n"],
                 "pct": round(s["n"] * 100 / svc_total)} for s in svc_counts]

    # Recent activity feed
    recent = (FlowEvent.objects
              .all().order_by("-created_at")[:80])

    # Top active users
    top_users = (FlowEvent.objects
                 .filter(created_at__gte=week_ago)
                 .values("mobile_number")
                 .annotate(n=Count("event_id"))
                 .order_by("-n")[:10])

    return _no_cache(render(request, "flowbuilder/dashboard.html", {
        "total_users": total_users,
        "users_24h": users_24h,
        "users_7d": users_7d,
        "msgs_24h_in": msgs_24h_in,
        "msgs_24h_out": msgs_24h_out,
        "services": services,
        "recent": recent,
        "top_users": top_users,
    }))


@login_required
@require_GET
def user_conversation_view(request: HttpRequest, mobile_number: str) -> HttpResponse:
    """Per-user conversation timeline. Shows every logged event in
    chronological order alongside the current FlowState."""
    import json as _json
    from .models import FlowEvent, FlowState as FSM

    state = FSM.objects.filter(mobile_number=mobile_number).first()
    ctx_pretty = ""
    if state:
        try:
            ctx_pretty = _json.dumps(_json.loads(state.context or "{}"),
                                     indent=2, ensure_ascii=False, default=str)
        except Exception:
            ctx_pretty = state.context

    events_qs = (FlowEvent.objects
                 .filter(mobile_number=mobile_number)
                 .order_by("-created_at")[:500])
    events = list(events_qs)
    events.reverse()   # show oldest → newest in the timeline

    return _no_cache(render(request, "flowbuilder/conversation.html", {
        "mobile_number": mobile_number,
        "state": state,
        "ctx_pretty": ctx_pretty,
        "events": events,
        "event_count": len(events),
    }))


@login_required
@require_GET
def users_view(request: HttpRequest) -> HttpResponse:
    """Read-only audit view: list every FlowState row with its current node
    and full context. Optional filter by phone number prefix. No write side
    effects — purely diagnostic."""
    import json as _json
    q = (request.GET.get("q") or "").strip()
    qs = FlowStateModel.objects.all().order_by("-updated_at")
    if q:
        qs = qs.filter(mobile_number__icontains=q)
    rows = []
    for s in qs[:200]:   # cap to keep the page small
        try:
            ctx = _json.loads(s.context or "{}")
        except Exception:
            ctx = {"_parse_error": s.context}
        rows.append({
            "mobile_number": s.mobile_number,
            "flow_id": s.flow_id,
            "current_node_id": s.current_node_id or "—",
            "updated_at": s.updated_at,
            "ctx_keys": sorted(ctx.keys()),
            "ctx_json": _json.dumps(ctx, indent=2, ensure_ascii=False, default=str),
        })
    return _no_cache(render(request, "flowbuilder/users.html",
                            {"rows": rows, "q": q, "total": qs.count()}))


@login_required
@require_GET
def builder(request: HttpRequest, flow_id: str) -> HttpResponse:
    try:
        flow = Flow.objects.get(flow_id=flow_id)
    except Flow.DoesNotExist:
        raise Http404(f"Flow '{flow_id}' not found")
    return _no_cache(render(request, "flowbuilder/builder.html", {"flow": flow}))


# ---------------------------------------------------------------------------
# Auth + Settings
# ---------------------------------------------------------------------------


SETTING_DEFINITIONS = [
    ("PICKY_TOKEN", True,  "Picky Assist API token (treat as secret)"),
    ("PICKY_APPLICATION", False, "Picky Assist application ID (e.g. 121)"),
    ("PICKY_URL", False, "Picky Assist push endpoint base URL"),
    ("BOT_WHATSAPP_NUMBER", False, "Bot's WhatsApp number for wa.me deep-links (no '+', e.g. 917842169111)"),
    ("DEFAULT_FLOW_ID", False, "Fallback flow id when no trigger matches and no live flow"),
    ("FALLBACK_WEBHOOK_URL", False, "If set, unmatched webhook calls forward here (legacy bridge)"),
    # ── Identi / CIMMYT integration ──
    ("CIMMYT_CLIENT_ID", True,  "Identi AuthX client_id (for OAuth2 client_credentials)"),
    ("CIMMYT_CLIENT_SECRET", True, "Identi AuthX client_secret"),
    ("AUTHX_URL", False, "AuthX OAuth2 token endpoint (default: https://authx2.d.identi.digital/oauth2/token)"),
    ("IDENTI_METRICS_URL", False, "Identi node-metrics base URL (default: https://node-metrics.d.identi.digital)"),
    ("CIMMYT_BACKEND_URL", False, "CIMMYT Backend base URL (default: https://cimmyt-service.d.identi.digital)"),
    ("CIMMYT_NODE_URL", False, "CIMMYT Node base URL (default: https://cimmyt-node.d.identi.digital)"),
    ("CIMMYT_ISSUER_DID", False, "Issuer DID for VC issuance (default: did:web:organizations.d.identi.digital:organizations:cimmyt)"),
    ("CIMMYT_BASE_URL", False, "CIMMYT internal API base for plot/logbook lists (default: https://aws.agroconsultasonline.com.ar)"),
    # ── Test / dev overrides ──
    ("TEST_NUMBERS", False, "Comma-separated phone numbers (no '+') that get test coordinates substituted when their real location is outside service coverage. Used so developers can exercise the weather/soil flows from anywhere. Example: 918700530369,919999999999"),
    ("TEST_COORD_LAT", False, "Substitute latitude for TEST_NUMBERS when out of coverage (default: 20.6195)"),
    ("TEST_COORD_LNG", False, "Substitute longitude for TEST_NUMBERS when out of coverage (default: -100.7898)"),
    ("TEST_CURP", False, "Substitute CURP for TEST_NUMBERS users when their wallet has no real CURP cert. Lets developers see the Generar UP/Bitácora picker populated without re-onboarding. Default: SAMJ541225HOCNNS03 (known-good producer in the doc)"),
    # ── Picky template IDs (per-environment) ──
    ("PICKY_TPL_WEATHER_LOC", False, "Picky-approved WhatsApp template ID for the weather location-capture CTA button. Defaults to PQ18478160 (production). Staging uses PQ18758860."),
    ("PICKY_TPL_SOIL_LOC", False, "Picky-approved WhatsApp template ID for the soil 'Mapas de Suelos' location-capture CTA button. Defaults to XJ18431679 (production). Staging uses XG18350179."),
]


@require_http_methods(["GET", "POST"])
def login_view(request: HttpRequest) -> HttpResponse:
    error = None
    if request.method == "POST":
        username = (request.POST.get("username") or "").strip()
        password = request.POST.get("password") or ""
        user = dj_auth.authenticate(request, username=username, password=password)
        if user:
            dj_auth.login(request, user)
            nxt = request.POST.get("next") or request.GET.get("next") or reverse("flow_builder_index")
            return redirect(nxt)
        error = "Invalid username or password"
    return _no_cache(render(request, "flowbuilder/login.html", {"error": error,
        "next": request.GET.get("next", "")}))


@require_http_methods(["GET", "POST"])
def logout_view(request: HttpRequest) -> HttpResponse:
    dj_auth.logout(request)
    return redirect("flow_login")


@login_required
@require_http_methods(["GET", "POST"])
def settings_view(request: HttpRequest) -> HttpResponse:
    if request.method == "POST":
        for key, _is_secret, _desc in SETTING_DEFINITIONS:
            val = (request.POST.get(key) or "").strip()
            # If user submits empty for a secret AND the existing value is set,
            # don't wipe it — UI shows "***" placeholder for set secrets.
            existing = AppSetting.objects.filter(key=key).first()
            if not val and existing and existing.is_secret and request.POST.get(f"{key}__keep") == "1":
                continue
            AppSetting.objects.update_or_create(
                key=key,
                defaults={"value": val, "is_secret": _is_secret, "description": _desc},
            )
        return redirect("flow_settings")

    rows = []
    for key, is_secret, desc in SETTING_DEFINITIONS:
        s = AppSetting.objects.filter(key=key).first()
        rows.append({
            "key": key,
            "value": (s.value if s else ""),
            "is_secret": is_secret,
            "description": desc,
            "is_set": bool(s and s.value),
        })
    return _no_cache(render(request, "flowbuilder/settings.html", {"rows": rows}))


@require_GET
def location_capture(request: HttpRequest) -> HttpResponse:
    """Public page that asks for browser geolocation, then opens wa.me with
    'Latitude:LAT & Longitude:LNG' pre-filled — user taps Send and the bot
    receives the coordinates as a regular text message.

    Reachable at /flow/loc/?phone=<user_number>
    """
    bot_number = AppSetting.objects.filter(key="BOT_WHATSAPP_NUMBER").first()
    bot_number = bot_number.value if bot_number else ""
    return _no_cache(render(request, "flowbuilder/location_capture.html", {
        "bot_number": bot_number,
        "user_phone": request.GET.get("phone", ""),
    }))


@csrf_exempt
@login_required
@require_POST
def api_flow_status(request: HttpRequest, flow_id: str) -> JsonResponse:
    """Toggle a flow between draft/live/archived via UI buttons."""
    body = json.loads(request.body or b"{}")
    new_status = int(body.get("status", -1))
    if new_status not in (Flow.STATUS_DRAFT, Flow.STATUS_LIVE, Flow.STATUS_ARCHIVED):
        return JsonResponse({"ok": False, "error": "invalid status"}, status=400)
    n = Flow.objects.filter(flow_id=flow_id).update(status=new_status)
    if not n:
        return JsonResponse({"ok": False, "error": "not found"}, status=404)
    return JsonResponse({"ok": True, "flow_id": flow_id, "status": new_status})


@csrf_exempt
@login_required
@require_http_methods(["GET", "POST"])
def api_flows(request: HttpRequest) -> JsonResponse:
    if request.method == "GET":
        flows = list(Flow.objects.all().values("flow_id", "name", "version", "status", "updated_at"))
        for f in flows:
            f["updated_at"] = f["updated_at"].isoformat() if f["updated_at"] else None
        return JsonResponse({"ok": True, "flows": flows})

    body = json.loads(request.body or b"{}")
    flow_id = (body.get("flow_id") or "").strip()
    name = body.get("name") or flow_id
    if not flow_id:
        return JsonResponse({"ok": False, "error": "flow_id required"}, status=400)
    if Flow.objects.filter(flow_id=flow_id).exists():
        return JsonResponse({"ok": False, "error": "flow_id already exists"}, status=409)

    starter = {
        "flow_id": flow_id,
        "name": name,
        "version": 1,
        "triggers": [{"type": "keyword", "match": ["start", "hi"], "next": "n_start"}],
        "nodes": [
            {"id": "n_start", "type": "send_text",
             "text": f"Welcome to {name}!", "next": "n_end",
             "position": {"x": 200, "y": 200}},
            {"id": "n_end", "type": "end",
             "position": {"x": 200, "y": 400}},
        ],
    }
    Flow.objects.create(flow_id=flow_id, name=name, definition=json.dumps(starter), version=1)
    return JsonResponse({"ok": True, "flow_id": flow_id})


@csrf_exempt
@login_required
@require_http_methods(["GET", "PUT", "DELETE"])
def api_flow(request: HttpRequest, flow_id: str) -> JsonResponse:
    if request.method == "GET":
        try:
            flow = Flow.objects.get(flow_id=flow_id)
        except Flow.DoesNotExist:
            return JsonResponse({"ok": False, "error": "not found"}, status=404)
        return JsonResponse({
            "ok": True,
            "flow_id": flow.flow_id,
            "name": flow.name,
            "version": flow.version,
            "definition": json.loads(flow.definition),
        })

    if request.method == "DELETE":
        deleted, _ = Flow.objects.filter(flow_id=flow_id).delete()
        FlowStateModel.objects.filter(flow_id=flow_id).delete()
        return JsonResponse({"ok": True, "deleted": deleted})

    body = json.loads(request.body or b"{}")
    definition = body.get("definition")
    if not isinstance(definition, dict):
        return JsonResponse({"ok": False, "error": "definition must be an object"}, status=400)

    try:
        flow = Flow.objects.get(flow_id=flow_id)
    except Flow.DoesNotExist:
        return JsonResponse({"ok": False, "error": "not found"}, status=404)

    definition["flow_id"] = flow_id
    flow.definition = json.dumps(definition)
    flow.version = (flow.version or 0) + 1
    if body.get("name"):
        flow.name = body["name"]
    flow.save()

    return JsonResponse({"ok": True, "version": flow.version})
