"""Dynamic Page Generation — Streamlit Monitor

Run:  uv run streamlit run app.py
"""
from __future__ import annotations

import html as _html
import os
import sys
import tempfile
import time
import urllib.parse
from pathlib import Path

import requests
import streamlit as st

sys.path.insert(0, str(Path(__file__).parent))
from runner import PipelineRunner
from gitlab_runner import GitLabRunner
from efs_runner import EFSSyncRunner
from tenant_runner import TenantRunner
from jenkins_runner import JenkinsRunner
from jenkins_trigger_runner import JenkinsTriggerRunner
from pipeline_runner_runner import PipelineRunnerRunner
from runner_trigger_runner import RunnerTriggerRunner
from db_runner import DbRunner
from setup_runner import SetupRunner
from build_runner import BuildRunner
from scripts_runner import RepoSetupRunner

# Load parent .env so API URL vars are available (no python-dotenv needed)
_PARENT_ENV = Path(__file__).resolve().parent.parent / ".env"
if _PARENT_ENV.exists():
    for _line in _PARENT_ENV.read_text(encoding="utf-8").splitlines():
        _line = _line.strip()
        if _line and not _line.startswith("#") and "=" in _line:
            _k, _, _v = _line.partition("=")
            _k = _k.strip()
            if _k and _k not in os.environ:
                os.environ[_k] = _v.strip()

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

STEPS = [
    ("step-01-input-ingestion",    "Input Ingestion"),
    ("step-02-prd-generation",     "PRD + DDL Generation"),
    ("step-02c-dalfin",            "Mantara Schema + Dalfin"),
    ("step-07-save-ddl-to-db",     "DDL → Database"),
    ("step-03-backend-generation", "Backend / API Generation"),
    ("step-04-ir-generation",      "IR Generation"),
    ("step-05-react-generation",   "React Generation"),
]
STEP_SEQUENCE = [s[0] for s in STEPS]
STEP_NUM_TO_KEY = {
    "01":  "step-01-input-ingestion",
    "02":  "step-02-prd-generation",
    "02c": "step-02c-dalfin",
    "07":  "step-07-save-ddl-to-db",
    "03":  "step-03-backend-generation",
    "04":  "step-04-ir-generation",
    "05":  "step-05-react-generation",
}
NEXT_STEP_NUM = {
    "02":  "01",
    "02c": "02",
    "07":  "02c",
    "03":  "07",
    "04":  "03",
    "05":  "04",
}


# ─── CSS ──────────────────────────────────────────────────────────────────────

st.set_page_config(
    page_title="DPG Pipeline Runner",
    page_icon="🚀",
    layout="wide",
)

st.markdown(
    """
<style>
.stApp { background: #0f172a; color: #f1f5f9; }
.step-pending { color: #6b7280; }
.step-running { color: #3b82f6; }
.step-done    { color: #10b981; }
.step-error   { color: #ef4444; }
.code-tight pre { font-size: 11px !important; padding: 8px !important; }
#MainMenu, footer, header { visibility: hidden; }
.stDeployButton, .stToolbar { display: none !important; }
</style>
""",
    unsafe_allow_html=True,
)


# ─── State ────────────────────────────────────────────────────────────────────

def _fresh_pipeline(run_id: str | None = None) -> dict:
    return {
        "run_id": run_id,
        "steps": {
            key: {
                "status": "pending",
                "duration_ms": None,
                "start_time": None,
                "input_tokens": 0,
                "output_tokens": 0,
                "total_tokens": 0,
                "llm_calls": 0,
                "llm_details": [],
                "retries": 0,
                "notes": {},
                "sub_steps": [],  # [{"msg": str, "done": bool}]
            }
            for key, _ in STEPS
        },
        "current_step": None,
        "logs": [],
        "project_root": None,
        "artifacts": {
            "ddl": None, "prd": False, "cir": False,
            "system_name": None, "api_endpoints": None,
            "api_modules": [], "ir_pages": [], "react_pages": [],
            "mantara_done": False, "ddl_to_db": None,
        },
        "totals": {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "llm_calls": 0},
        "current_llm": None,
        "log_path": None,
    }


def init_state(run_id: str | None = None) -> None:
    defaults = {
        "runner": None, "status": "idle",
        "pipeline": _fresh_pipeline(run_id), "_started": set(),
        "scripts_runner": None, "scripts_status": "idle",
        "scripts_dest": None, "scripts_logs": [], "_scripts_trigger": False,
        "gitlab_runner": None, "gitlab_status": "idle",
        "gitlab_url": None, "gitlab_logs": [], "_gitlab_trigger": False,
        "efs_runner": None, "efs_status": "idle",
        "efs_logs": [], "efs_dest": None, "_efs_trigger": False,
        "tenant_runner": None, "tenant_status": "idle",
        "tenant_result": None, "tenant_logs": [], "_tenant_trigger": False,
        "jenkins_runner": None, "jenkins_status": "idle",
        "jenkins_url": None, "jenkins_logs": [], "_jenkins_trigger": False,
        "jenkins_trig_runner": None, "jenkins_trig_status": "idle",
        "jenkins_trig_url": None, "jenkins_trig_logs": [], "_jenkins_trig_trigger": False,
        "pr_runner": None, "pr_status": "idle",
        "pr_url": None, "pr_logs": [], "_pr_trigger": False,
        "pr_trig_runner": None, "pr_trig_status": "idle",
        "pr_trig_url": None, "pr_trig_logs": [], "_pr_trig_trigger": False,
        "db_runner": None, "db_status": "idle",
        "db_logs": [], "_db_trigger": False,
        "setup_runner": None, "setup_status": "idle",
        "setup_logs": [], "_setup_trigger": False,
        "build_runner": None, "build_status": "idle",
        "build_logs": [], "_build_trigger": False,
    }
    for k, v in defaults.items():
        if k not in st.session_state:
            st.session_state[k] = v


# ─── Sub-step helpers ─────────────────────────────────────────────────────────

def _push_substep(p: dict, step_key: str | None, msg: str) -> None:
    """Mark the last active sub-step done, then append msg as the new active one."""
    sk = step_key or p.get("current_step")
    if not sk or sk not in p["steps"]:
        return
    items = p["steps"][sk]["sub_steps"]
    if items and not items[-1]["done"]:
        items[-1]["done"] = True
    items.append({"msg": msg, "done": False})


def _done_substep(p: dict, step_key: str | None, msg: str) -> None:
    """Append msg as an immediately-completed sub-step."""
    sk = step_key or p.get("current_step")
    if not sk or sk not in p["steps"]:
        return
    p["steps"][sk]["sub_steps"].append({"msg": msg, "done": True})


def _finish_substeps(p: dict, step_key: str) -> None:
    """Mark all remaining active sub-steps as done when a step completes."""
    for item in p["steps"][step_key]["sub_steps"]:
        item["done"] = True


# ─── Event processing ─────────────────────────────────────────────────────────

def process_events() -> None:
    runner: PipelineRunner = st.session_state.runner
    if runner is None:
        return
    p       = st.session_state.pipeline
    started = st.session_state._started

    for ev in runner.drain():
        t = ev["type"]

        if t == "log":
            p["logs"].append(ev["text"])
            if len(p["logs"]) > 700:
                p["logs"] = p["logs"][-700:]

        elif t == "run_id":
            if not p["run_id"]:
                p["run_id"] = ev["run_id"]
            _done_substep(p, STEP_NUM_TO_KEY["01"], f"Run created: {ev['run_id']}")

        elif t == "substep":
            _push_substep(p, p["current_step"], ev["message"])

        elif t == "step_print":
            num = ev["step_num"]
            key = STEP_NUM_TO_KEY.get(num)
            if num not in started:
                started.add(num)
                prev = NEXT_STEP_NUM.get(num)
                if prev:
                    pk = STEP_NUM_TO_KEY.get(prev)
                    if pk and p["steps"][pk]["status"] == "running":
                        s = p["steps"][pk]
                        s["status"] = "done"
                        if s["start_time"]:
                            s["duration_ms"] = (time.time() - s["start_time"]) * 1000
                        _finish_substeps(p, pk)
                if key and p["steps"][key]["status"] == "pending":
                    p["steps"][key]["status"] = "running"
                    p["steps"][key]["start_time"] = time.time()
                    p["current_step"] = key
                    _push_substep(p, key, ev["message"])

        elif t == "llm_calling":
            p["current_llm"] = f"{ev['model']}  {ev.get('label','')}"
            label = ev.get("label", "").strip()
            msg = f"Calling {ev['model']}" + (f": {label}" if label else "")
            _push_substep(p, p["current_step"], msg)

        elif t == "llm_call":
            p["current_llm"] = None
            sk = _resolve_step_key(ev["step"], p["current_step"])
            if sk:
                s = p["steps"][sk]
                s["llm_calls"]     += 1
                s["input_tokens"]  += ev["input_tokens"]
                s["output_tokens"] += ev["output_tokens"]
                s["total_tokens"]  += ev["total_tokens"]
                if ev["attempt"] > 1:
                    s["retries"] += 1
                s["llm_details"].append({
                    "model":   ev["model"],
                    "attempt": f"{ev['attempt']}/{ev['total_attempts']}",
                    "page":    ev.get("page") or "—",
                    "in":      ev["input_tokens"],
                    "out":     ev["output_tokens"],
                    "total":   ev["total_tokens"],
                    "ms":      int(ev["duration_ms"]),
                })
                # mark the "calling…" sub-step as done
                items = s["sub_steps"]
                if items and not items[-1]["done"]:
                    items[-1]["done"] = True
                if ev.get("page") and sk == "step-05-react-generation":
                    if ev["page"] not in p["artifacts"]["react_pages"]:
                        p["artifacts"]["react_pages"].append(ev["page"])
                        _done_substep(p, sk, f"Page done: {ev['page']}")
            tot = p["totals"]
            tot["input_tokens"]  += ev["input_tokens"]
            tot["output_tokens"] += ev["output_tokens"]
            tot["total_tokens"]  += ev["total_tokens"]
            tot["llm_calls"]     += 1

        elif t == "artifact":
            if ev["name"] == "ddl":
                p["artifacts"]["ddl"] = ev.get("filename", "schema.sql")
                _done_substep(p, STEP_NUM_TO_KEY["02"], f"DDL generated: {ev.get('filename', 'schema.sql')}")
            elif ev["name"] == "prd":
                p["artifacts"]["prd"] = True
                _done_substep(p, STEP_NUM_TO_KEY["02"], "PRD generated")

        elif t == "backend_info":
            a = p["artifacts"]
            a["system_name"]   = ev["system"]
            a["api_endpoints"] = ev["endpoints"]
            sk = STEP_NUM_TO_KEY["03"]
            p["steps"][sk]["notes"].update(
                {"system": ev["system"], "modules": ev["modules"], "endpoints": ev["endpoints"]}
            )
            _done_substep(p, sk, f"System: {ev['system']} — {ev['endpoints']} endpoints")

        elif t == "mantara_done":
            p["artifacts"]["mantara_done"] = True
            _done_substep(p, STEP_NUM_TO_KEY["02c"], "mantara_schema.json + dalfin.json generated")

        elif t == "ddl_to_db":
            sk = STEP_NUM_TO_KEY["07"]
            if ev["success"]:
                p["artifacts"]["ddl_to_db"] = "success"
                _done_substep(p, sk, "DDL deployed to database")
            else:
                p["artifacts"]["ddl_to_db"] = "failed"
                _done_substep(p, sk, "DDL deployment failed — continuing without DB")

        elif t == "ir_pages":
            p["artifacts"]["ir_pages"] = ev["pages"]
            p["artifacts"]["cir"] = True
            pages = ev["pages"]
            preview = ", ".join(pages[:3]) + ("…" if len(pages) > 3 else "")
            _done_substep(p, STEP_NUM_TO_KEY["04"], f"IR ready: {len(pages)} pages ({preview})")

        elif t == "api_module":
            m = ev["module"]
            if m not in p["artifacts"]["api_modules"]:
                p["artifacts"]["api_modules"].append(m)
                _done_substep(p, STEP_NUM_TO_KEY["03"], f"Module generated: {m}")

        elif t == "tenant_update":
            st.session_state.tenant_status = ev["status"]
            st.session_state.tenant_logs.append(ev["text"])
            if ev.get("result"):
                st.session_state.tenant_result = ev["result"]

        elif t == "project_done":
            p["project_root"] = ev["project_root"]
            _done_substep(p, STEP_NUM_TO_KEY["05"], f"Project built: {Path(ev['project_root']).name}")

        elif t == "run_summary":
            tot = p["totals"]
            tot["input_tokens"]  = ev["total_input"]
            tot["output_tokens"] = ev["total_output"]
            tot["total_tokens"]  = ev["total_input"] + ev["total_output"]
            tot["llm_calls"]     = ev["llm_calls"]

        elif t == "finished":
            rc = ev["return_code"]
            st.session_state.status = "done" if rc == 0 else "error"
            for key, _ in STEPS:
                s = p["steps"][key]
                if s["status"] == "running":
                    s["status"] = "done" if rc == 0 else "error"
                    if s["start_time"]:
                        s["duration_ms"] = (time.time() - s["start_time"]) * 1000
                    _finish_substeps(p, key)
            p["current_step"] = None
            p["current_llm"]  = None
            if ev.get("log_path"):
                p["log_path"] = ev["log_path"]


def process_gitlab_events() -> None:
    runner: GitLabRunner = st.session_state.gitlab_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.gitlab_logs.append(ev["text"])
        elif t == "gitlab_done":
            st.session_state.gitlab_status = "done"
            st.session_state.gitlab_url = ev["url"]
        elif t == "gitlab_error":
            st.session_state.gitlab_status = "error"
        elif t == "finished":
            if st.session_state.gitlab_status not in ("done", "error"):
                st.session_state.gitlab_status = "done" if ev["return_code"] == 0 else "error"


def process_setup_events() -> None:
    runner: SetupRunner = st.session_state.setup_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.setup_logs.append(ev["text"])
        elif t == "setup_done":
            st.session_state.setup_status = "done"
        elif t == "setup_error":
            st.session_state.setup_status = "error"
        elif t == "finished":
            if st.session_state.setup_status not in ("done", "error"):
                st.session_state.setup_status = "done" if ev["return_code"] == 0 else "error"


def process_build_events() -> None:
    runner: BuildRunner = st.session_state.build_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.build_logs.append(ev["text"])
        elif t == "build_done":
            st.session_state.build_status = "done"
        elif t == "build_error":
            st.session_state.build_status = "error"
        elif t == "finished":
            if st.session_state.build_status not in ("done", "error"):
                st.session_state.build_status = "done" if ev["return_code"] == 0 else "error"


def process_db_events() -> None:
    runner: DbRunner = st.session_state.db_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.db_logs.append(ev["text"])
        elif t == "db_done":
            st.session_state.db_status = "done"
        elif t == "db_error":
            st.session_state.db_status = "error"
        elif t == "finished":
            if st.session_state.db_status not in ("done", "error"):
                st.session_state.db_status = "done" if ev["return_code"] == 0 else "error"


def process_jenkins_events() -> None:
    runner: JenkinsRunner = st.session_state.jenkins_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.jenkins_logs.append(ev["text"])
        elif t == "jenkins_done":
            st.session_state.jenkins_status = "done"
            st.session_state.jenkins_url = ev["url"]
        elif t == "jenkins_error":
            st.session_state.jenkins_status = "error"
        elif t == "finished":
            if st.session_state.jenkins_status not in ("done", "error"):
                st.session_state.jenkins_status = "done" if ev["return_code"] == 0 else "error"


def process_pr_events() -> None:
    runner: PipelineRunnerRunner = st.session_state.pr_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.pr_logs.append(ev["text"])
        elif t == "runner_done":
            st.session_state.pr_status = "done"
            st.session_state.pr_url = ev["url"]
        elif t == "runner_error":
            st.session_state.pr_status = "error"
        elif t == "finished":
            if st.session_state.pr_status not in ("done", "error"):
                st.session_state.pr_status = "done" if ev["return_code"] == 0 else "error"


def process_jenkins_trig_events() -> None:
    runner: JenkinsTriggerRunner = st.session_state.jenkins_trig_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.jenkins_trig_logs.append(ev["text"])
        elif t == "jenkins_trig_done":
            st.session_state.jenkins_trig_status = "done"
            st.session_state.jenkins_trig_url = ev["url"]
        elif t == "jenkins_trig_error":
            st.session_state.jenkins_trig_status = "error"
        elif t == "finished":
            if st.session_state.jenkins_trig_status not in ("done", "error"):
                st.session_state.jenkins_trig_status = "done" if ev["return_code"] == 0 else "error"


def process_runner_trig_events() -> None:
    runner: RunnerTriggerRunner = st.session_state.pr_trig_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.pr_trig_logs.append(ev["text"])
        elif t == "runner_trig_done":
            st.session_state.pr_trig_status = "done"
            st.session_state.pr_trig_url = ev["url"]
        elif t == "runner_trig_error":
            st.session_state.pr_trig_status = "error"
        elif t == "finished":
            if st.session_state.pr_trig_status not in ("done", "error"):
                st.session_state.pr_trig_status = "done" if ev["return_code"] == 0 else "error"


def process_tenant_events() -> None:
    runner: TenantRunner = st.session_state.tenant_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.tenant_logs.append(ev["text"])
        elif t == "tenant_done":
            st.session_state.tenant_status = "done"
            st.session_state.tenant_result = ev["result"]
        elif t == "tenant_error":
            st.session_state.tenant_status = "error"
        elif t == "finished":
            if st.session_state.tenant_status not in ("done", "error"):
                st.session_state.tenant_status = "done" if ev["return_code"] == 0 else "error"


def process_efs_events() -> None:
    runner: EFSSyncRunner = st.session_state.efs_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.efs_logs.append(ev["text"])
            if len(st.session_state.efs_logs) > 200:
                st.session_state.efs_logs = st.session_state.efs_logs[-200:]
        elif t == "efs_done":
            st.session_state.efs_dest = ev["dest"]
        elif t == "finished":
            st.session_state.efs_status = "done" if ev["return_code"] == 0 else "error"


def process_scripts_events() -> None:
    runner: ScriptsRunner = st.session_state.scripts_runner
    if runner is None:
        return
    for ev in runner.drain():
        t = ev["type"]
        if t == "log":
            st.session_state.scripts_logs.append(ev["text"])
        elif t == "scripts_done":
            st.session_state.scripts_status = "done"
            st.session_state.scripts_dest = ev["dest"]
        elif t == "scripts_error":
            st.session_state.scripts_status = "error"
        elif t == "finished":
            if st.session_state.scripts_status not in ("done", "error"):
                st.session_state.scripts_status = "done" if ev["return_code"] == 0 else "error"


def _resolve_step_key(step_field: str, current: str | None) -> str | None:
    for k in STEP_SEQUENCE:
        if k in step_field or step_field in k:
            return k
    return current


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

def _step_emoji(status: str) -> str:
    return {"pending": "⏳", "running": "🔵", "done": "✅", "error": "❌"}.get(status, "⏳")


def render_substeps(sub_steps: list[dict], status: str) -> None:
    items = sub_steps[-25:]
    if not items:
        label = "_Starting…_" if status == "running" else "_Waiting…_"
        st.caption(label)
        return

    lines = []
    for i, item in enumerate(items):
        is_active = not item["done"] and i == len(items) - 1 and status == "running"
        if item["done"]:
            icon, color = "✓", "#10b981"
        elif is_active:
            icon, color = "●", "#60a5fa"
        else:
            icon, color = "○", "#6b7280"
        esc = _html.escape(item["msg"])
        lines.append(
            f'<div style="color:{color};font-size:.78rem;line-height:1.9;'
            f'font-family:ui-monospace,Menlo,monospace;">'
            f'<span style="margin-right:6px">{icon}</span>{esc}</div>'
        )

    st.markdown(
        '<div style="background:#020817;border-radius:8px;padding:10px 14px;">'
        + "".join(lines)
        + "</div>",
        unsafe_allow_html=True,
    )


def render_log(logs: list[str]) -> None:
    import streamlit.components.v1 as components

    def _e(s: str) -> str:
        return _html.escape(str(s))

    if not logs:
        components.html(
            '<div style="background:#020817;border-radius:12px;padding:14px 16px;height:300px;'
            'font-family:ui-monospace,Menlo,monospace;font-size:.72rem;color:#475569;font-style:italic;">'
            'Waiting for output…</div>',
            height=300,
        )
        return

    lines_html = ""
    for line in logs[-140:]:
        esc = _e(line)
        if "[LLM]" in line:
            color = "#50FA7B"
        elif "started" in line and "step-0" in line:
            color = "#BD93F9"
        elif "completed" in line and "duration_ms" in line:
            color = "#F1FA8C"
        elif "run_id" in line or "[run_id]" in line:
            color = "#FF79C6"
        elif "[0" in line and "]" in line:
            color = "#8BE9FD"
        elif "ERROR" in line or ("error" in line.lower() and "duration" not in line):
            color = "#FF5555"
        elif "WARNING" in line:
            color = "#FFB86C"
        else:
            color = "#C9D1D9"
        lines_html += f'<div style="color:{color};line-height:1.75;">{esc}</div>'

    html = f"""<!DOCTYPE html><html><body style="margin:0;padding:0;background:#020817;">
<div id="log" style="background:#020817;padding:14px 16px;height:280px;overflow-y:auto;
  font-family:ui-monospace,'Cascadia Code','JetBrains Mono',Menlo,monospace;
  font-size:.72rem;box-sizing:border-box;">
{lines_html}
</div>
<script>var el=document.getElementById('log');el.scrollTop=el.scrollHeight;</script>
</body></html>"""

    components.html(html, height=300, scrolling=False)


# ─── Domain validation ────────────────────────────────────────────────────────

def _normalize_domain_url(domain_url: str) -> str:
    """Ensure the URL has a scheme so urlparse can extract a hostname."""
    url = domain_url.strip()
    if url and "://" not in url:
        url = "https://" + url
    return url


def _validate_domain_url(domain_url: str) -> tuple[bool, str, dict]:
    """Call the system-builder domain-info API.

    Returns (ok, error_message, data).
    """
    api_url = os.environ.get("SYSTEM_BUILDER_API_URL", "").strip()
    if not api_url:
        return False, "SYSTEM_BUILDER_API_URL is not set in the environment.", {}
    normalized = _normalize_domain_url(domain_url)
    try:
        resp = requests.post(api_url, json={"domain_url": normalized}, timeout=10)
        if resp.status_code == 200:
            return True, "", resp.json()
        try:
            body = resp.json()
            # API wraps errors as {"detail": {"message": "..."}} or {"message": "..."}
            detail = (
                body.get("message")
                or (body.get("detail") or {}).get("message")
                or resp.text
            )
        except Exception:
            detail = resp.text
        return False, f"Domain validation failed: {detail}", {}
    except requests.exceptions.ConnectionError:
        return False, "Could not reach the domain-info API. Check your network.", {}
    except requests.exceptions.Timeout:
        return False, "Domain-info API timed out.", {}
    except Exception as exc:
        return False, f"Domain validation error: {exc}", {}


# ─── Main ─────────────────────────────────────────────────────────────────────

def main() -> None:
    qp_run_id  = st.query_params.get("run_id")
    qp_tenant_id = st.query_params.get("tenant_id")
    qp_domain_url = st.query_params.get("domain")
    qp_user_prompt = urllib.parse.unquote_plus(st.query_params.get("user_prompt", ""))
    init_state(qp_run_id)
    if st.session_state.runner:
        process_events()
    if st.session_state.tenant_runner:
        process_tenant_events()
    if st.session_state.setup_runner:
        process_setup_events()
    if st.session_state.build_runner:
        process_build_events()
    if st.session_state.db_runner:
        process_db_events()
    if st.session_state.gitlab_runner:
        process_gitlab_events()
    if st.session_state.jenkins_runner:
        process_jenkins_events()
    if st.session_state.jenkins_trig_runner:
        process_jenkins_trig_events()
    if st.session_state.pr_runner:
        process_pr_events()
    if st.session_state.pr_trig_runner:
        process_runner_trig_events()
    if st.session_state.efs_runner:
        process_efs_events()
    if st.session_state.scripts_runner:
        process_scripts_events()

    p = st.session_state.pipeline
    if qp_run_id:
        p["run_id"] = qp_run_id

    app_status = st.session_state.status
    running    = app_status == "running"

    # ── Sidebar ───────────────────────────────────────────────────────────────
    with st.sidebar:
        st.header("📥 Input")

        domain = st.text_input(
            "Domain",
            placeholder="e.g. Healthcare, E-commerce, Finance…",
            disabled=running,
            key="domain_input",
        )

        domain_url = st.text_input(
            "Domain URL",
            value=qp_domain_url or "",
            placeholder="e.g. myapp.com or https://myapp.com",
            disabled=running,
            key="domain_url_input",
        )

        prompt = st.text_area(
            "Prompt",
            value=qp_user_prompt or "",
            placeholder=(
                "Describe the application you want to generate…\n\n"
                "e.g. A warehouse management system with inventory tracking, "
                "order management, and analytics dashboard."
            ),
            height=160,
            disabled=running,
            key="prompt_input",
        )

        uploaded = st.file_uploader(
            "Attach file (PDF, PPTX, DOCX, or image)",
            type=["pdf", "pptx", "docx", "png", "jpg", "jpeg", "gif", "webp"],
            disabled=running,
            help="Attach a deck, spec PDF, Word doc, or mockup image",
        )

        run_id_input = st.text_input(
            "Run ID (optional)",
            value=qp_run_id or "",
            placeholder="leave blank to auto-generate",
            disabled=running,
            key="run_id_input",
        )

        st.divider()

        run_btn = st.button(
            "▶️  Run pipeline",
            type="primary",
            use_container_width=True,
            disabled=running or (not prompt and not uploaded),
        )
        stop_btn = st.button(
            "■  Stop",
            use_container_width=True,
            disabled=not running,
        )

        if p["project_root"]:
            st.divider()
            st.caption(f"📁 `{p['project_root']}`")
        if p["log_path"]:
            st.caption(f"📄 `{p['log_path']}`")


    # ── Top header ────────────────────────────────────────────────────────────
    top_left, top_right = st.columns([1, 1])
    with top_left:
        st.title("🚀 Dynamic Page Generation")
        st.caption("Prompt or file → 5 pipeline steps → React app")
    with top_right:
        if p["run_id"]:
            st.metric("Run ID", p["run_id"])

    # ── Progress banner ───────────────────────────────────────────────────────
    if running:
        n_done = sum(1 for k, _ in STEPS if p["steps"][k]["status"] == "done")
        st.info(f"🔵  Pipeline running — {n_done}/{len(STEPS)} steps complete. See step cards below for live progress.")
    elif app_status == "done":
        st.success("✅  Pipeline complete — see Artifacts & Output below.")
    elif app_status == "error":
        st.error("❌  Pipeline failed — check the Live Log tab for details.")

    # ── Pipeline Progress ─────────────────────────────────────────────────────
    st.subheader("📊 Pipeline Progress")

    for idx, (key, display) in enumerate(STEPS, 1):
        s      = p["steps"][key]
        status = s["status"]
        emoji  = _step_emoji(status)

        title = f"{emoji} Step {idx} — {display}"
        if s["duration_ms"]:
            title += f"  ({s['duration_ms'] / 1000:.1f}s)"
        elif status == "running" and s["start_time"]:
            elapsed = time.time() - s["start_time"]
            title += f"  ({elapsed:.0f}s…)"

        expanded = status in ("running", "error")
        with st.expander(title, expanded=expanded):
            col_log, col_metrics = st.columns([2, 1])

            with col_log:
                st.caption("Activity")
                render_substeps(s["sub_steps"], status)

                if s["llm_details"]:
                    with st.expander(f"LLM calls ({s['llm_calls']})", expanded=False):
                        lines = []
                        for d in s["llm_details"]:
                            lines.append(
                                f"[{d['attempt']}] {d['model']}  page={d['page']}  "
                                f"in={d['in']:,} out={d['out']:,} total={d['total']:,} ({d['ms']}ms)"
                            )
                        st.code("\n".join(lines[-30:]), language="text")

            with col_metrics:
                items: list[str] = []
                if s["total_tokens"]:
                    items.append(f"- ⚡ `tokens`: **{s['total_tokens']:,}**")
                if s["input_tokens"]:
                    items.append(f"- ↑ `input`: **{s['input_tokens']:,}**")
                if s["output_tokens"]:
                    items.append(f"- ↓ `output`: **{s['output_tokens']:,}**")
                if s["llm_calls"]:
                    items.append(f"- 🤖 `calls`: **{s['llm_calls']}**")
                if s["retries"]:
                    items.append(f"- ↻ `retries`: **{s['retries']}**")

                notes = s.get("notes", {})
                if notes.get("system"):
                    items.append(f"- 🏗 `system`: **{notes['system']}**")
                if notes.get("endpoints"):
                    items.append(f"- 🔌 `endpoints`: **{notes['endpoints']}**")

                if items:
                    st.markdown("\n".join(items))
                else:
                    st.caption("_pending_")

            st.divider()

            # Per-step intermediate artifacts
            art = p["artifacts"]
            art_lines: list[str] = []
            if key == "step-01-input-ingestion" and status in ("done", "error"):
                if p["run_id"]:
                    art_lines.append(f"- run_id: `{p['run_id']}`")
            elif key == "step-02-prd-generation":
                if art["prd"]:
                    art_lines.append("- ✅ `PRD.md` generated")
                if art["ddl"]:
                    art_lines.append(f"- ✅ `{art['ddl']}` (DDL schema)")
            elif key == "step-02c-dalfin":
                if art["mantara_done"]:
                    art_lines.append("- ✅ `mantara_schema.json` generated")
                    art_lines.append("- ✅ `dalfin.json` compiled")
                elif status in ("done", "error"):
                    art_lines.append("- ⚠️ Mantara/Dalfin skipped")
            elif key == "step-07-save-ddl-to-db":
                if art["ddl_to_db"] == "success":
                    art_lines.append("- ✅ DDL deployed to database")
                elif art["ddl_to_db"] == "failed":
                    art_lines.append("- ⚠️ DDL deployment failed — pipeline continued")
                elif status in ("done", "error"):
                    art_lines.append("- ⚠️ DDL deployment skipped")
            elif key == "step-03-backend-generation" and art["system_name"]:
                art_lines.append(f"- 🏗 System: `{art['system_name']}`")
                if art["api_endpoints"]:
                    art_lines.append(f"- 🔌 `{art['api_endpoints']}` endpoints")
                if art["api_modules"]:
                    mods = art["api_modules"]
                    art_lines.append(
                        f"- 📦 `{len(mods)}` modules: "
                        + " · ".join(f"`{m}`" for m in mods[:6])
                        + ("…" if len(mods) > 6 else "")
                    )
            elif key == "step-04-ir-generation" and art["ir_pages"]:
                pgs = art["ir_pages"]
                art_lines.append(
                    f"- 📄 `{len(pgs)}` IR pages: "
                    + " · ".join(f"`{pg}`" for pg in pgs[:5])
                    + ("…" if len(pgs) > 5 else "")
                )
                if art["cir"]:
                    art_lines.append("- ✅ CIR generated")
            elif key == "step-05-react-generation" and art["react_pages"]:
                pgs = art["react_pages"]
                art_lines.append(
                    f"- ⚛️ `{len(pgs)}` React pages: "
                    + " · ".join(f"`{pg}`" for pg in pgs[:5])
                    + ("…" if len(pgs) > 5 else "")
                )

            if art_lines:
                st.markdown("\n".join(art_lines))
            else:
                st.caption("_artifacts will appear here after the step finishes_")

    # ── Final output ──────────────────────────────────────────────────────────
    st.subheader("🎯 Artifacts & Output")
    final_tabs = st.tabs(["Artifacts", "Token Usage", "Live Log"])

    with final_tabs[0]:
        art = p["artifacts"]
        has_any = any([
            art["ddl"], art["prd"], art["cir"],
            art["system_name"], art["ir_pages"], art["react_pages"],
        ])
        if p["log_path"]:
            log_p = Path(p["log_path"])
            st.markdown(f"**Stdout log:** `{log_p}`")
            proj_log = (Path(p["project_root"]) / "logs" / "run_log.md") if p["project_root"] else None
            if proj_log and proj_log.exists():
                st.markdown(f"**Run log:** `{proj_log}`")
            st.divider()

        if has_any:
            lines = []
            if art["ddl"]:
                lines.append(f"- ✅ DDL Schema: `{art['ddl']}`")
            if art["prd"]:
                lines.append("- ✅ PRD: `PRD.md` generated")
            if art["cir"]:
                lines.append("- ✅ CIR: intermediate representation generated")
            if art["system_name"]:
                ep = f" ({art['api_endpoints']} endpoints)" if art["api_endpoints"] else ""
                lines.append(f"- 🏗 System: `{art['system_name']}`{ep}")
            if art["api_modules"]:
                mods = art["api_modules"]
                lines.append(f"- 📦 API Modules ({len(mods)}): " + ", ".join(f"`{m}`" for m in mods))
            if art["ir_pages"]:
                pgs = art["ir_pages"]
                lines.append(f"- 📄 IR Pages ({len(pgs)}): " + ", ".join(f"`{pg}`" for pg in pgs))
            if art["react_pages"]:
                pgs = art["react_pages"]
                lines.append(f"- ⚛️ React Pages ({len(pgs)}): " + ", ".join(f"`{pg}`" for pg in pgs))
            st.markdown("\n".join(lines))
        else:
            st.info("Artifacts will appear here as the pipeline runs.")

    with final_tabs[1]:
        t = p["totals"]
        if t["total_tokens"] > 0:
            c1, c2, c3, c4 = st.columns(4)
            c1.metric("Total Tokens", f"{t['total_tokens']:,}")
            c2.metric("Input ↑",      f"{t['input_tokens']:,}")
            c3.metric("Output ↓",     f"{t['output_tokens']:,}")
            c4.metric("LLM Calls",    t["llm_calls"])

            st.divider()
            rows = []
            for key, display in STEPS:
                s = p["steps"][key]
                if s["total_tokens"] > 0 or s["status"] != "pending":
                    rows.append({
                        "Step":      display,
                        "Status":    s["status"].capitalize(),
                        "Duration":  f"{s['duration_ms']/1000:.1f}s" if s["duration_ms"] else "—",
                        "Calls":     s["llm_calls"] or "—",
                        "Total tok": f"{s['total_tokens']:,}" if s["total_tokens"] else "—",
                        "Input ↑":   f"{s['input_tokens']:,}" if s["input_tokens"] else "—",
                        "Output ↓":  f"{s['output_tokens']:,}" if s["output_tokens"] else "—",
                        "Retries":   s["retries"] or "—",
                    })
            if rows:
                import pandas as pd
                st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
        else:
            st.info("Token usage will appear here during the run.")

    with final_tabs[2]:
        render_log(p["logs"])

    # ── management-tenant step card ───────────────────────────────────────────
    tenant_status = st.session_state.tenant_status
    t_emoji = {"running": "🔵", "done": "✅", "error": "❌"}.get(tenant_status, "⏳")
    t_title = f"{t_emoji} management-tenant — Ensure Tenant"
    if tenant_status == "done" and st.session_state.tenant_result:
        t_title += f"  →  {st.session_state.tenant_result}"
    with st.expander(t_title, expanded=tenant_status in ("running", "error", "idle")):
        col_t, col_tbtn, col_tvscode = st.columns([3, 1, 1])
        with col_t:
            tenant_input = st.text_input(
                "Tenant",
                value=qp_tenant_id or "",
                placeholder="freemeal",
                disabled=tenant_status == "running",
                key="tenant_input",
            )
        with col_tbtn:
            st.write("")
            st.write("")
            tenant_btn = st.button(
                "✔  Ensure",
                use_container_width=True,
                disabled=tenant_status == "running" or not tenant_input,
            )
            if tenant_btn:
                st.session_state._tenant_trigger = True
        with col_tvscode:
            st.write("")
            st.write("")
            _t_val = tenant_input or qp_tenant_id or ""
            _run_id_val = qp_run_id or (Path(p["project_root"]).name if p["project_root"] else "")
            if _t_val and _run_id_val:
                _vscode_url = f"https://{_t_val}.code.llmatica.dalfin.ai/?folder=/home/coder/workspace/{_run_id_val}"
                st.link_button("💻  Open VS Code", _vscode_url, use_container_width=True)
            else:
                st.button("💻  Open VS Code", disabled=True, use_container_width=True)
        if tenant_status in ("running", "done", "error"):
            render_log(st.session_state.tenant_logs)

    # ── EFS sync step card ────────────────────────────────────────────────────
    efs_status = st.session_state.efs_status
    efs_emoji  = {"running": "🔵", "done": "✅", "error": "❌"}.get(efs_status, "⏳")
    efs_title  = f"{efs_emoji} management-code-efs — EFS Sync"
    if efs_status == "done" and st.session_state.efs_dest:
        efs_title += f"  →  {st.session_state.efs_dest}"
    with st.expander(efs_title, expanded=efs_status in ("running", "error", "idle")):
        col_inputs, col_btn = st.columns([3, 1])
        with col_inputs:
            efs_run_id_input = st.text_input(
                "Run ID / project slug",
                value=qp_run_id or (Path(p["project_root"]).name if p["project_root"] else ""),
                placeholder="bmi_calculator_application_1778480873055",
                disabled=efs_status == "running",
                key="efs_run_id_input",
            )
            efs_env_input = st.text_input(
                "EFS environment",
                value=qp_tenant_id or "",
                placeholder="freemeal",
                disabled=efs_status == "running",
                key="efs_env_input",
            )
        with col_btn:
            st.write("")
            st.write("")
            efs_btn = st.button(
                "⬆️  Sync",
                use_container_width=True,
                disabled=efs_status == "running" or not efs_env_input or not efs_run_id_input,
            )
            if efs_btn:
                st.session_state._efs_trigger = True
        if efs_status in ("running", "done", "error"):
            render_log(st.session_state.efs_logs)

    # ── management-code-scripts step card ────────────────────────────────────
    scripts_status = st.session_state.scripts_status
    sc_emoji = {"running": "🔵", "done": "✅", "error": "❌"}.get(scripts_status, "⏳")
    sc_title = f"{sc_emoji} repo-setup — Patch DB Env + Generate K8s Manifests"
    if scripts_status == "done" and st.session_state.scripts_dest:
        sc_title += f"  →  {st.session_state.scripts_dest}"
    with st.expander(sc_title, expanded=scripts_status in ("running", "error")):
        col_sc, col_scbtn = st.columns([3, 1])
        with col_sc:
            sc_run_id_input = st.text_input(
                "Run ID / project slug",
                value=qp_run_id or (Path(p["project_root"]).name if p["project_root"] else ""),
                placeholder="bmi_calculator_application_1778480873055",
                disabled=scripts_status == "running",
                key="sc_run_id_input",
            )
        with col_scbtn:
            st.write("")
            st.write("")
            scripts_btn = st.button(
                "📝  Generate",
                use_container_width=True,
                disabled=scripts_status == "running" or not sc_run_id_input,
            )
            if scripts_btn:
                st.session_state._scripts_trigger = True
        if scripts_status in ("running", "done", "error"):
            render_log(st.session_state.scripts_logs)

    # ── management-code-gitlab step card ─────────────────────────────────────
    gitlab_status = st.session_state.gitlab_status
    gl_emoji = {"running": "🔵", "done": "✅", "error": "❌"}.get(gitlab_status, "⏳")
    gl_title = f"{gl_emoji} management-code-gitlab — GitLab Push"
    if gitlab_status == "done" and st.session_state.gitlab_url:
        gl_title += f"  →  {st.session_state.gitlab_url}"
    with st.expander(gl_title, expanded=gitlab_status in ("running", "error", "idle")):
        col_gl, col_glbtn = st.columns([3, 1])
        with col_gl:
            gl_run_id_input = st.text_input(
                "Run ID / project slug",
                value=qp_run_id or (Path(p["project_root"]).name if p["project_root"] else ""),
                placeholder="bmi_calculator_application_1778480873055",
                disabled=gitlab_status == "running",
                key="gl_run_id_input",
            )
            gl_org_code_input = st.text_input(
                "Org code (tenant)",
                value=qp_tenant_id or "",
                placeholder="freemeal",
                disabled=gitlab_status == "running",
                key="gl_org_code_input",
            )
        with col_glbtn:
            st.write("")
            st.write("")
            gitlab_btn = st.button(
                "🚀  Push",
                use_container_width=True,
                disabled=gitlab_status == "running" or not gl_run_id_input,
            )
            if gitlab_btn:
                st.session_state._gitlab_trigger = True
        if gitlab_status in ("running", "done", "error"):
            render_log(st.session_state.gitlab_logs)

    # ── management-code-jenkins step card ─────────────────────────────────────
    jenkins_status = st.session_state.jenkins_status
    jenkins_trig_status = st.session_state.jenkins_trig_status
    jk_emoji = {"running": "🔵", "done": "✅", "error": "❌"}.get(jenkins_status, "⏳")
    jk_title = f"{jk_emoji} management-code-jenkins — Jenkins Pipeline"
    if jenkins_status == "done" and st.session_state.jenkins_url:
        jk_title += f"  →  {st.session_state.jenkins_url}"
    with st.expander(jk_title, expanded=jenkins_status in ("running", "error")):
        col_jk, col_jkbtn, col_jktrig = st.columns([3, 1, 1])
        with col_jk:
            jk_run_id_input = st.text_input(
                "Run ID / project slug",
                value=qp_run_id or (Path(p["project_root"]).name if p["project_root"] else ""),
                placeholder="bmi_calculator_application_1778480873055",
                disabled=jenkins_status == "running",
                key="jk_run_id_input",
            )
            jk_gitlab_url_input = st.text_input(
                "GitLab group URL",
                value=st.session_state.gitlab_url or "",
                placeholder="https://gitlab.example.com/freemeal/my-project",
                disabled=jenkins_status == "running",
                key="jk_gitlab_url_input",
            )
        with col_jkbtn:
            st.write("")
            st.write("")
            jenkins_btn = st.button(
                "⚙️  Setup",
                use_container_width=True,
                disabled=jenkins_status == "running" or not jk_run_id_input or not jk_gitlab_url_input,
            )
            if jenkins_btn:
                st.session_state._jenkins_trigger = True
        with col_jktrig:
            st.write("")
            st.write("")
            jk_job_select = st.selectbox(
                "Job",
                ["frontend", "backend"],
                key="jk_job_select",
                label_visibility="collapsed",
            )
            jenkins_trig_btn = st.button(
                "⚡  Trigger",
                use_container_width=True,
                disabled=jenkins_trig_status == "running" or not jk_run_id_input,
                key="jenkins_trig_btn",
            )
            if jenkins_trig_btn:
                st.session_state._jenkins_trig_trigger = True
        if jenkins_status in ("running", "done", "error"):
            render_log(st.session_state.jenkins_logs)
        if jenkins_trig_status in ("running", "done", "error"):
            trig_label = "⚡ Trigger log"
            if st.session_state.jenkins_trig_url:
                trig_label += f"  →  {st.session_state.jenkins_trig_url}"
            st.caption(trig_label)
            render_log(st.session_state.jenkins_trig_logs)

    # ── management-code-runner step card ──────────────────────────────────────
    pr_status = st.session_state.pr_status
    pr_trig_status = st.session_state.pr_trig_status
    pr_emoji  = {"running": "🔵", "done": "✅", "error": "❌"}.get(pr_status, "⏳")
    pr_title  = f"{pr_emoji} management-code-runner — Runner Pipeline"
    if pr_status == "done" and st.session_state.pr_url:
        pr_title += f"  →  {st.session_state.pr_url}"
    with st.expander(pr_title, expanded=pr_status in ("running", "error")):
        col_pr, col_prbtn, col_prtrig = st.columns([3, 1, 1])
        with col_pr:
            pr_run_id_input = st.text_input(
                "Run ID / project slug",
                value=qp_run_id or (Path(p["project_root"]).name if p["project_root"] else ""),
                placeholder="bmi_calculator_application_1778480873055",
                disabled=pr_status == "running",
                key="pr_run_id_input",
            )
            pr_gitlab_url_input = st.text_input(
                "GitLab group URL",
                value=st.session_state.gitlab_url or "",
                placeholder="https://gitlab.example.com/freemeal/my-project",
                disabled=pr_status == "running",
                key="pr_gitlab_url_input",
            )
        with col_prbtn:
            st.write("")
            st.write("")
            pr_btn = st.button(
                "▶️  Setup",
                use_container_width=True,
                disabled=pr_status == "running" or not pr_run_id_input or not pr_gitlab_url_input,
            )
            if pr_btn:
                st.session_state._pr_trigger = True
        with col_prtrig:
            st.write("")
            st.write("")
            pr_job_select = st.selectbox(
                "Job",
                ["frontend", "backend"],
                key="pr_job_select",
                label_visibility="collapsed",
            )
            pr_trig_btn = st.button(
                "⚡  Trigger",
                use_container_width=True,
                disabled=pr_trig_status == "running" or not pr_run_id_input,
                key="pr_trig_btn",
            )
            if pr_trig_btn:
                st.session_state._pr_trig_trigger = True
        if pr_status in ("running", "done", "error"):
            render_log(st.session_state.pr_logs)
        if pr_trig_status in ("running", "done", "error"):
            trig_label = "⚡ Trigger log"
            if st.session_state.pr_trig_url:
                trig_label += f"  →  {st.session_state.pr_trig_url}"
            st.caption(trig_label)
            render_log(st.session_state.pr_trig_logs)

    # ── management-db step card ───────────────────────────────────────────────
    db_status = st.session_state.db_status
    db_emoji  = {"running": "🔵", "done": "✅", "error": "❌"}.get(db_status, "⏳")
    db_title  = f"{db_emoji} management-db — Database Migrations"
    with st.expander(db_title, expanded=db_status in ("running", "error")):
        col_db, col_dbbtn = st.columns([3, 1])
        with col_db:
            db_run_id_input = st.text_input(
                "Run ID / project slug",
                value=qp_run_id or (Path(p["project_root"]).name if p["project_root"] else ""),
                placeholder="bmi_calculator_application_1778480873055",
                disabled=db_status == "running",
                key="db_run_id_input",
            )
        with col_dbbtn:
            st.write("")
            st.write("")
            db_btn = st.button(
                "🗄️  Migrate",
                use_container_width=True,
                disabled=db_status == "running" or not db_run_id_input,
            )
            if db_btn:
                st.session_state._db_trigger = True
        if db_status in ("running", "done", "error"):
            render_log(st.session_state.db_logs)

    # ── management-code-setup step card ──────────────────────────────────────
    setup_status = st.session_state.setup_status
    su_emoji = {"running": "🔵", "done": "✅", "error": "❌"}.get(setup_status, "⏳")
    su_title = f"{su_emoji} management-code-setup — Install Dependencies"
    with st.expander(su_title, expanded=setup_status in ("running", "error")):
        col_su, col_subtn = st.columns([3, 1])
        with col_su:
            su_run_id_input = st.text_input(
                "Run ID / project slug",
                value=qp_run_id or (Path(p["project_root"]).name if p["project_root"] else ""),
                placeholder="bmi_calculator_application_1778480873055",
                disabled=setup_status == "running",
                key="su_run_id_input",
            )
        with col_subtn:
            st.write("")
            st.write("")
            setup_btn = st.button(
                "📦  Setup",
                use_container_width=True,
                disabled=setup_status == "running" or not su_run_id_input,
            )
            if setup_btn:
                st.session_state._setup_trigger = True
        if setup_status in ("running", "done", "error"):
            render_log(st.session_state.setup_logs)

    # ── management-code-build step card ──────────────────────────────────────
    build_status = st.session_state.build_status
    bd_emoji = {"running": "🔵", "done": "✅", "error": "❌"}.get(build_status, "⏳")
    bd_title = f"{bd_emoji} management-code-build — Build"
    with st.expander(bd_title, expanded=build_status in ("running", "error")):
        col_bd, col_bdbtn = st.columns([3, 1])
        with col_bd:
            bd_run_id_input = st.text_input(
                "Run ID / project slug",
                value=qp_run_id or (Path(p["project_root"]).name if p["project_root"] else ""),
                placeholder="bmi_calculator_application_1778480873055",
                disabled=build_status == "running",
                key="bd_run_id_input",
            )
        with col_bdbtn:
            st.write("")
            st.write("")
            build_btn = st.button(
                "🏗️  Build",
                use_container_width=True,
                disabled=build_status == "running" or not bd_run_id_input,
            )
            if build_btn:
                st.session_state._build_trigger = True
        if build_status in ("running", "done", "error"):
            render_log(st.session_state.build_logs)

    # ── Button handlers ───────────────────────────────────────────────────────
    if run_btn:
        db_info: dict = {}
        canonical_domain_url = _normalize_domain_url(domain_url) if domain_url and domain_url.strip() else ""
        if canonical_domain_url:
            ok, err_msg, db_info = _validate_domain_url(canonical_domain_url)
            if not ok:
                st.sidebar.error(err_msg)
                st.stop()

        file_path: Path | None = None
        if uploaded:
            suffix = Path(uploaded.name).suffix
            tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix, prefix="dpg_upload_")
            tmp.write(uploaded.getvalue())
            tmp.close()
            file_path = Path(tmp.name)

        st.session_state.pipeline = _fresh_pipeline(qp_run_id)
        st.session_state._started = set()
        runner = PipelineRunner()
        runner.start(
            prompt=prompt or "",
            file_path=file_path,
            domain=domain or "",
            domain_url=canonical_domain_url,
            db_info=db_info,
            run_id=run_id_input.strip() or None,
        )
        st.session_state.runner = runner
        st.session_state.status = "running"
        st.rerun()

    if stop_btn and st.session_state.runner:
        st.session_state.runner.terminate()
        st.session_state.status = "error"
        for key, _ in STEPS:
            s = p["steps"][key]
            if s["status"] == "running":
                s["status"] = "error"
                if s["start_time"]:
                    s["duration_ms"] = (time.time() - s["start_time"]) * 1000
        st.rerun()

    # ── Auto-triggers (set flags before handlers so same rerun processes them) ──

    # pipeline done → tenant
    _tenant_available = qp_tenant_id or st.session_state.get("tenant_input", "").strip()
    if (app_status == "done"
            and _tenant_available
            and st.session_state.tenant_status == "idle"):
        st.session_state._tenant_trigger = True

    # tenant done (or no tenant) → EFS sync (copy everything to shared workspace)
    _efs_env_available = qp_tenant_id or st.session_state.get("efs_env_input", "").strip()
    _efs_ready_to_start = (
        st.session_state.tenant_status == "done"
        or (app_status == "done" and not _tenant_available)
    )
    if (_efs_ready_to_start
            and p["project_root"]
            and _efs_env_available
            and st.session_state.efs_status == "idle"):
        st.session_state._efs_trigger = True

    # EFS done → repo-setup (patch DB_HOST in EFS .env + generate Dockerfile + k8s manifests)
    _efs_dest = st.session_state.efs_dest or ""
    _scripts_ready = (
        st.session_state.efs_status == "done"
        or (_efs_ready_to_start and not _efs_env_available)
    )
    _sc_slug = (Path(p["project_root"]).name if p["project_root"] else "") or st.session_state.get("sc_run_id_input", "").strip()
    if (_scripts_ready
            and (_efs_dest or _sc_slug)
            and st.session_state.scripts_status == "idle"):
        st.session_state._scripts_trigger = True

    # scripts done (or no scripts slug) → gitlab (push EFS workspace to GitLab)
    _gl_slug = (Path(p["project_root"]).name if p["project_root"] else "") or st.session_state.get("gl_run_id_input", "").strip()
    _gitlab_ready = (
        st.session_state.scripts_status == "done"
        or (_scripts_ready and not _efs_dest and not _sc_slug)
    )
    if (_gitlab_ready
            and (_efs_dest or _gl_slug)
            and st.session_state.gitlab_status == "idle"):
        st.session_state._gitlab_trigger = True

    # gitlab done → jenkins
    _jk_gitlab_url = st.session_state.gitlab_url or ""
    _jk_slug = (Path(p["project_root"]).name if p["project_root"] else "") or st.session_state.get("jk_run_id_input", "").strip()
    if (st.session_state.gitlab_status == "done"
            and _jk_gitlab_url
            and _jk_slug
            and st.session_state.jenkins_status == "idle"):
        st.session_state._jenkins_trigger = True

    # jenkins done → runner
    _pr_gitlab_url = st.session_state.gitlab_url or ""
    _pr_slug = (Path(p["project_root"]).name if p["project_root"] else "") or st.session_state.get("pr_run_id_input", "").strip()
    if (st.session_state.jenkins_status == "done"
            and _pr_gitlab_url
            and _pr_slug
            and st.session_state.pr_status == "idle"):
        st.session_state._pr_trigger = True

    # runner done (or no runner) → db (migrate from EFS backend dir using 127.0.0.1 SSH tunnel)
    _db_slug = (Path(p["project_root"]).name if p["project_root"] else "") or st.session_state.get("db_run_id_input", "").strip()
    _db_ready = (
        st.session_state.pr_status == "done"
        or (st.session_state.gitlab_status == "done" and not _pr_gitlab_url and not _pr_slug)
    )
    if (_db_ready
            and (_efs_dest or _db_slug)
            and st.session_state.db_status == "idle"):
        st.session_state._db_trigger = True

    # db done (or no db) → setup (install deps in EFS workspace)
    _su_slug = (Path(p["project_root"]).name if p["project_root"] else "") or st.session_state.get("su_run_id_input", "").strip()
    _setup_ready = (
        st.session_state.db_status == "done"
        or (_db_ready and not _efs_dest and not _db_slug)
    )
    if (_setup_ready
            and (_efs_dest or _su_slug)
            and st.session_state.setup_status == "idle"):
        st.session_state._setup_trigger = True

    # setup done → build (build frontend in EFS workspace)
    _bd_slug = (Path(p["project_root"]).name if p["project_root"] else "") or st.session_state.get("bd_run_id_input", "").strip()
    if (st.session_state.setup_status == "done"
            and (_efs_dest or _bd_slug)
            and st.session_state.build_status == "idle"):
        st.session_state._build_trigger = True

    # ── Trigger handlers ──────────────────────────────────────────────────────

    if st.session_state.get("_setup_trigger"):
        st.session_state._setup_trigger = False
        _su_efs = st.session_state.efs_dest or ""
        su_slug = st.session_state.get("su_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        su_efs_env = qp_tenant_id or st.session_state.get("efs_env_input", "").strip() or None
        if _su_efs or su_slug:
            st.session_state.setup_logs = []
            st.session_state.setup_status = "running"
            su_r = SetupRunner()
            su_r.start(project_slug=su_slug, efs_env=su_efs_env, project_dir=_su_efs or None)
            st.session_state.setup_runner = su_r
            st.rerun()

    if st.session_state.get("_build_trigger"):
        st.session_state._build_trigger = False
        _bd_efs = st.session_state.efs_dest or ""
        bd_slug = st.session_state.get("bd_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        bd_efs_env = qp_tenant_id or st.session_state.get("efs_env_input", "").strip() or None
        if _bd_efs or bd_slug:
            st.session_state.build_logs = []
            st.session_state.build_status = "running"
            bd_r = BuildRunner()
            bd_r.start(project_slug=bd_slug, efs_env=bd_efs_env, project_dir=_bd_efs or None)
            st.session_state.build_runner = bd_r
            st.rerun()

    if st.session_state.get("_tenant_trigger"):
        st.session_state._tenant_trigger = False
        tenant_val = st.session_state.get("tenant_input", "").strip() or qp_tenant_id or ""
        if tenant_val:
            st.session_state.tenant_logs = []
            st.session_state.tenant_result = None
            st.session_state.tenant_status = "running"
            t_runner = TenantRunner()
            t_runner.start(tenant_name=tenant_val)
            st.session_state.tenant_runner = t_runner
            st.rerun()

    if st.session_state.get("_efs_trigger"):
        st.session_state._efs_trigger = False
        project_slug = st.session_state.get("efs_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        efs_env_val = st.session_state.get("efs_env_input", "").strip() or qp_tenant_id or ""
        if efs_env_val and project_slug:
            st.session_state.efs_logs = []
            st.session_state.efs_dest = None
            st.session_state.efs_status = "running"
            efs_runner = EFSSyncRunner()
            efs_runner.start(project_slug=project_slug, efs_env=efs_env_val)
            st.session_state.efs_runner = efs_runner
            st.rerun()

    if st.session_state.get("_scripts_trigger"):
        st.session_state._scripts_trigger = False
        _sc_efs = st.session_state.efs_dest or ""
        sc_slug = st.session_state.get("sc_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        if _sc_efs or sc_slug:
            st.session_state.scripts_logs = []
            st.session_state.scripts_dest = None
            st.session_state.scripts_status = "running"
            sc_r = RepoSetupRunner()
            sc_r.start(project_dir=_sc_efs or None, project_slug=sc_slug or None)
            st.session_state.scripts_runner = sc_r
            st.rerun()

    if st.session_state.get("_gitlab_trigger"):
        st.session_state._gitlab_trigger = False
        _gl_efs = st.session_state.efs_dest or ""
        gl_slug = st.session_state.get("gl_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        gl_org = st.session_state.get("gl_org_code_input", "").strip() or qp_tenant_id or None
        if _gl_efs or gl_slug:
            st.session_state.gitlab_logs = []
            st.session_state.gitlab_url = None
            st.session_state.gitlab_status = "running"
            gl_runner = GitLabRunner()
            gl_runner.start(project_dir=_gl_efs or None, project_slug=gl_slug or None, org_code=gl_org)
            st.session_state.gitlab_runner = gl_runner
            st.rerun()

    if st.session_state.get("_jenkins_trigger"):
        st.session_state._jenkins_trigger = False
        jk_slug = st.session_state.get("jk_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        jk_gl_url = st.session_state.get("jk_gitlab_url_input", "").strip() or st.session_state.gitlab_url or ""
        if jk_slug and jk_gl_url:
            st.session_state.jenkins_logs = []
            st.session_state.jenkins_url = None
            st.session_state.jenkins_status = "running"
            jk_runner = JenkinsRunner()
            jk_runner.start(project_slug=jk_slug, gitlab_url=jk_gl_url)
            st.session_state.jenkins_runner = jk_runner
            st.rerun()

    if st.session_state.get("_pr_trigger"):
        st.session_state._pr_trigger = False
        pr_slug = st.session_state.get("pr_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        pr_gl_url = st.session_state.get("pr_gitlab_url_input", "").strip() or st.session_state.gitlab_url or ""
        if pr_slug and pr_gl_url:
            st.session_state.pr_logs = []
            st.session_state.pr_url = None
            st.session_state.pr_status = "running"
            pr_runner = PipelineRunnerRunner()
            pr_runner.start(project_slug=pr_slug, gitlab_url=pr_gl_url)
            st.session_state.pr_runner = pr_runner
            st.rerun()

    if st.session_state.get("_jenkins_trig_trigger"):
        st.session_state._jenkins_trig_trigger = False
        jk_trig_slug = st.session_state.get("jk_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        jk_trig_job = st.session_state.get("jk_job_select", "frontend")
        if jk_trig_slug:
            st.session_state.jenkins_trig_logs = []
            st.session_state.jenkins_trig_url = None
            st.session_state.jenkins_trig_status = "running"
            jk_trig_r = JenkinsTriggerRunner()
            jk_trig_r.start(project_slug=jk_trig_slug, job=jk_trig_job)
            st.session_state.jenkins_trig_runner = jk_trig_r
            st.rerun()

    if st.session_state.get("_pr_trig_trigger"):
        st.session_state._pr_trig_trigger = False
        pr_trig_slug = st.session_state.get("pr_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        pr_trig_job = st.session_state.get("pr_job_select", "frontend")
        if pr_trig_slug:
            st.session_state.pr_trig_logs = []
            st.session_state.pr_trig_url = None
            st.session_state.pr_trig_status = "running"
            pr_trig_r = RunnerTriggerRunner()
            pr_trig_r.start(project_slug=pr_trig_slug, job=pr_trig_job)
            st.session_state.pr_trig_runner = pr_trig_r
            st.rerun()

    if st.session_state.get("_db_trigger"):
        st.session_state._db_trigger = False
        _db_efs = st.session_state.efs_dest or ""
        db_slug = st.session_state.get("db_run_id_input", "").strip() or (
            Path(p["project_root"]).name if p["project_root"] else ""
        )
        if _db_efs or db_slug:
            st.session_state.db_logs = []
            st.session_state.db_status = "running"
            db_r = DbRunner()
            _db_backend = str(Path(_db_efs) / "backend") if _db_efs else None
            db_r.start(backend_dir=_db_backend, project_slug=db_slug or None)
            st.session_state.db_runner = db_r
            st.rerun()

    if (app_status == "running"
            or st.session_state.setup_status == "running"
            or st.session_state.tenant_status == "running"
            or st.session_state.efs_status == "running"
            or st.session_state.scripts_status == "running"
            or st.session_state.gitlab_status == "running"
            or st.session_state.jenkins_status == "running"
            or st.session_state.jenkins_trig_status == "running"
            or st.session_state.pr_status == "running"
            or st.session_state.pr_trig_status == "running"
            or st.session_state.db_status == "running"
            or st.session_state.build_status == "running"):
        time.sleep(1)
        st.rerun()


if __name__ == "__main__":
    main()
