"""
First-run setup wizard orchestrator.
Shown by app.py when user.yaml is absent OR wizard_complete is False.
Seven steps: hardware → tier → identity → resume → inference → search → integrations (optional).
Steps 1-6 are mandatory; step 7 is optional and can be skipped.
Each step writes to user.yaml on "Next" for crash recovery.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent.parent))

import streamlit as st
import yaml

from app.cloud_session import resolve_session, get_db_path, get_config_dir
resolve_session("peregrine")

_ROOT       = Path(__file__).parent.parent.parent
CONFIG_DIR  = get_config_dir()   # per-user dir in cloud; repo config/ locally
USER_YAML   = CONFIG_DIR / "user.yaml"
STEPS       = 6  # mandatory steps
STEP_LABELS = ["Hardware", "Tier", "Resume", "Identity", "Inference", "Search"]


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

def _load_yaml() -> dict:
    if USER_YAML.exists():
        return yaml.safe_load(USER_YAML.read_text()) or {}
    return {}


def _save_yaml(updates: dict) -> None:
    existing = _load_yaml()
    existing.update(updates)
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    USER_YAML.write_text(
        yaml.dump(existing, default_flow_style=False, allow_unicode=True)
    )


def _detect_gpus() -> list[str]:
    """Detect GPUs. Prefers env vars written by preflight (works inside Docker)."""
    import os
    import subprocess
    # Preflight writes PEREGRINE_GPU_NAMES to .env; compose passes it to the container.
    # This is the reliable path when running inside Docker without nvidia-smi access.
    env_names = os.environ.get("PEREGRINE_GPU_NAMES", "").strip()
    if env_names:
        return [n.strip() for n in env_names.split(",") if n.strip()]
    # Fallback: try nvidia-smi directly (works when running bare or with GPU passthrough)
    try:
        out = subprocess.check_output(
            ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
            text=True, timeout=5,
        )
        return [l.strip() for l in out.strip().splitlines() if l.strip()]
    except Exception:
        return []


def _suggest_profile(gpus: list[str]) -> str:
    import os
    # If preflight already ran and wrote a profile recommendation, use it.
    recommended = os.environ.get("RECOMMENDED_PROFILE", "").strip()
    if recommended:
        return recommended
    if len(gpus) >= 2:
        return "dual-gpu"
    if len(gpus) == 1:
        return "single-gpu"
    return "remote"


def _submit_wizard_task(section: str, input_data: dict) -> int:
    """Submit a wizard_generate background task. Returns task_id."""
    from scripts.task_runner import submit_task
    params = json.dumps({"section": section, "input": input_data})
    task_id, _ = submit_task(get_db_path(), "wizard_generate", 0, params=params)
    return task_id


def _poll_wizard_task(section: str) -> dict | None:
    """Return the most recent wizard_generate task row for a given section, or None."""
    import sqlite3
    conn = sqlite3.connect(get_db_path())
    conn.row_factory = sqlite3.Row
    row = conn.execute(
        "SELECT * FROM background_tasks "
        "WHERE task_type='wizard_generate' AND params LIKE ? "
        "ORDER BY id DESC LIMIT 1",
        (f'%"section": "{section}"%',),
    ).fetchone()
    conn.close()
    return dict(row) if row else None


def _generation_widget(section: str, label: str, tier: str,
                        feature_key: str, input_data: dict) -> str | None:
    """Render a generation button + polling fragment.

    Returns the generated result string if completed and not yet applied, else None.
    Call this inside a step to add LLM generation support.
    The caller decides whether to auto-populate a field with the result.
    """
    from app.wizard.tiers import can_use, tier_label as tl, has_configured_llm

    _has_byok = has_configured_llm()
    if not can_use(tier, feature_key, has_byok=_has_byok):
        st.caption(f"{tl(feature_key, has_byok=_has_byok)} {label}")
        return None

    col_btn, col_fb = st.columns([2, 5])
    if col_btn.button(f"\u2728 {label}", key=f"gen_{section}"):
        _submit_wizard_task(section, input_data)
        st.rerun()

    with st.expander("\u270f\ufe0f Request changes (optional)", expanded=False):
        prev = st.session_state.get(f"_gen_result_{section}", "")
        feedback = st.text_area(
            "Describe what to change", key=f"_feedback_{section}",
            placeholder="e.g. Make it shorter and emphasise leadership",
            height=60,
        )
        if prev and st.button(f"\u21ba Regenerate with feedback", key=f"regen_{section}"):
            _submit_wizard_task(section, {**input_data,
                                          "previous_result": prev,
                                          "feedback": feedback})
            st.rerun()

    # Polling fragment
    result_key = f"_gen_result_{section}"

    @st.fragment(run_every=3)
    def _poll():
        task = _poll_wizard_task(section)
        if not task:
            return
        status = task.get("status")
        if status in ("queued", "running"):
            stage = task.get("stage") or "Queued"
            st.info(f"\u23f3 {stage}\u2026")
        elif status == "completed":
            payload = json.loads(task.get("error") or "{}")
            result = payload.get("result", "")
            if result and result != st.session_state.get(result_key):
                st.session_state[result_key] = result
                st.rerun()
        elif status == "failed":
            st.warning(f"Generation failed: {task.get('error', 'unknown error')}")

    _poll()

    return st.session_state.get(result_key)


# ── Wizard state init ──────────────────────────────────────────────────────────

if "wizard_step" not in st.session_state:
    saved = _load_yaml()
    last_completed = saved.get("wizard_step", 0)
    st.session_state.wizard_step = min(last_completed + 1, STEPS + 1)  # resume at next step

step = st.session_state.wizard_step
saved_yaml = _load_yaml()
_tier = saved_yaml.get("dev_tier_override") or saved_yaml.get("tier", "free")

st.title("\U0001f44b Welcome to Peregrine")
st.caption("Complete the setup to start your job search. Progress saves automatically.")
st.progress(
    min((step - 1) / STEPS, 1.0),
    text=f"Step {min(step, STEPS)} of {STEPS}" if step <= STEPS else "Almost done!",
)
st.divider()


# ── Step 1: Hardware ───────────────────────────────────────────────────────────
if step == 1:
    from app.cloud_session import CLOUD_MODE as _CLOUD_MODE
    if _CLOUD_MODE:
        # Cloud deployment: always single-gpu (Heimdall), skip hardware selection
        _save_yaml({"inference_profile": "single-gpu", "wizard_step": 1})
        st.session_state.wizard_step = 2
        st.rerun()

    from app.wizard.step_hardware import validate, PROFILES

    st.subheader("Step 1 \u2014 Hardware Detection")
    gpus = _detect_gpus()
    suggested = _suggest_profile(gpus)

    if gpus:
        st.success(f"Detected {len(gpus)} GPU(s): {', '.join(gpus)}")
    else:
        st.info("No NVIDIA GPUs detected. 'Remote' or 'CPU' mode recommended.")

    profile = st.selectbox(
        "Inference mode", PROFILES, index=PROFILES.index(suggested),
        help="Controls which Docker services start. Change later in Settings \u2192 Services.",
    )
    if profile in ("single-gpu", "dual-gpu") and not gpus:
        st.warning(
            "No GPUs detected \u2014 GPU profiles require the NVIDIA Container Toolkit. "
            "See README for install instructions."
        )

    if st.button("Next \u2192", type="primary", key="hw_next"):
        errs = validate({"inference_profile": profile})
        if errs:
            st.error("\n".join(errs))
        else:
            _save_yaml({"inference_profile": profile, "wizard_step": 1})
            st.session_state.wizard_step = 2
            st.rerun()


# ── Step 2: Tier ───────────────────────────────────────────────────────────────
elif step == 2:
    from app.cloud_session import CLOUD_MODE as _CLOUD_MODE
    if _CLOUD_MODE:
        # Cloud mode: tier already resolved from Heimdall at session init
        cloud_tier = st.session_state.get("cloud_tier", "free")
        _save_yaml({"tier": cloud_tier, "wizard_step": 2})
        st.session_state.wizard_step = 3
        st.rerun()

    from app.wizard.step_tier import validate

    st.subheader("Step 2 \u2014 Choose Your Plan")
    st.caption(
        "**Free** is fully functional for self-hosted local use. "
        "**Paid/Premium** unlock LLM-assisted features."
    )

    tier_options = {
        "free":    "\U0001f193 **Free** \u2014 Local discovery, apply workspace, interviews kanban",
        "paid":    "\U0001f4bc **Paid** \u2014 + AI career summary, company research, email classifier, calendar sync",
        "premium": "\u2b50 **Premium** \u2014 + Voice guidelines, model fine-tuning, multi-user",
    }
    from app.wizard.tiers import TIERS
    current_tier = saved_yaml.get("tier", "free")
    selected_tier = st.radio(
        "Plan",
        list(tier_options.keys()),
        format_func=lambda x: tier_options[x],
        index=TIERS.index(current_tier) if current_tier in TIERS else 0,
    )

    col_back, col_next = st.columns([1, 4])
    if col_back.button("\u2190 Back", key="tier_back"):
        st.session_state.wizard_step = 1
        st.rerun()
    if col_next.button("Next \u2192", type="primary", key="tier_next"):
        errs = validate({"tier": selected_tier})
        if errs:
            st.error("\n".join(errs))
        else:
            _save_yaml({"tier": selected_tier, "wizard_step": 2})
            st.session_state.wizard_step = 3
            st.rerun()


# ── Step 3: Resume ─────────────────────────────────────────────────────────────
elif step == 3:
    from app.wizard.step_resume import validate

    st.subheader("Step 3 \u2014 Resume")
    st.caption("Upload your resume for fast parsing, or build it section by section.")

    # Read LinkedIn import result before tabs render (spec: "at step render time")
    _li_data = st.session_state.pop("_linkedin_extracted", None)
    if _li_data:
        st.session_state["_parsed_resume"] = _li_data

    tab_upload, tab_builder, tab_linkedin = st.tabs([
        "\U0001f4ce Upload", "\U0001f4dd Build Manually", "\U0001f517 LinkedIn"
    ])

    with tab_upload:
        uploaded = st.file_uploader("Upload PDF, DOCX, or ODT", type=["pdf", "docx", "odt"])
        if uploaded and st.button("Parse Resume", type="primary", key="parse_resume"):
            from scripts.resume_parser import (
                extract_text_from_pdf, extract_text_from_docx,
                extract_text_from_odt, structure_resume,
            )
            file_bytes = uploaded.read()
            ext = uploaded.name.rsplit(".", 1)[-1].lower()
            if ext == "pdf":
                raw_text = extract_text_from_pdf(file_bytes)
            elif ext == "odt":
                raw_text = extract_text_from_odt(file_bytes)
            else:
                raw_text = extract_text_from_docx(file_bytes)
            with st.spinner("Parsing\u2026"):
                parsed, parse_err = structure_resume(raw_text)

            # Diagnostic: show raw extraction + detected fields regardless of outcome
            with st.expander("🔍 Parse diagnostics", expanded=not bool(parsed and any(
                parsed.get(k) for k in ("name", "experience", "skills")
            ))):
                st.caption("**Raw extracted text (first 800 chars)**")
                st.code(raw_text[:800] if raw_text else "(empty)", language="text")
                if parsed:
                    st.caption("**Detected fields**")
                    st.json({k: (v[:3] if isinstance(v, list) else v) for k, v in parsed.items()})

            if parsed and any(parsed.get(k) for k in ("name", "experience", "skills")):
                st.session_state["_parsed_resume"] = parsed
                st.session_state["_raw_resume_text"] = raw_text
                _save_yaml({"_raw_resume_text": raw_text[:8000]})
                st.success("Parsed! Review the builder tab to edit entries.")
            elif parsed:
                # Parsed but empty — show what we got and let them proceed or build manually
                st.session_state["_parsed_resume"] = parsed
                st.warning("Resume text was extracted but no fields were recognised. "
                           "Check the diagnostics above — the section headers may use unusual labels. "
                           "You can still fill in the Build tab manually.")
            else:
                st.warning("Auto-parse failed \u2014 switch to the Build tab and add entries manually.")
                if parse_err:
                    st.caption(f"Reason: {parse_err}")

    with tab_builder:
        parsed = st.session_state.get("_parsed_resume", {})
        experience = st.session_state.get(
            "_experience",
            parsed.get("experience") or saved_yaml.get("experience", []),
        )

        for i, entry in enumerate(experience):
            with st.expander(
                f"{entry.get('title', 'Entry')} @ {entry.get('company', '?')}",
                expanded=(i == len(experience) - 1),
            ):
                entry["company"] = st.text_input("Company", entry.get("company", ""), key=f"co_{i}")
                entry["title"]   = st.text_input("Title",   entry.get("title",   ""), key=f"ti_{i}")
                raw_bullets = st.text_area(
                    "Responsibilities (one per line)",
                    "\n".join(entry.get("bullets", [])),
                    key=f"bu_{i}", height=80,
                )
                entry["bullets"] = [b.strip() for b in raw_bullets.splitlines() if b.strip()]
                if st.button("Remove entry", key=f"rm_{i}"):
                    experience.pop(i)
                    st.session_state["_experience"] = experience
                    st.rerun()

        if st.button("\uff0b Add work experience entry", key="add_exp"):
            experience.append({"company": "", "title": "", "bullets": []})
            st.session_state["_experience"] = experience
            st.rerun()

        # Bullet expansion generation
        if experience:
            all_bullets = "\n".join(
                b for e in experience for b in e.get("bullets", [])
            )
            _generation_widget(
                section="expand_bullets",
                label="Expand bullet points",
                tier=_tier,
                feature_key="llm_expand_bullets",
                input_data={"bullet_notes": all_bullets},
            )

    with tab_linkedin:
        from app.components.linkedin_import import render_linkedin_tab
        render_linkedin_tab(config_dir=CONFIG_DIR, tier=_tier)

    col_back, col_next = st.columns([1, 4])
    if col_back.button("\u2190 Back", key="resume_back"):
        st.session_state.wizard_step = 2
        st.rerun()
    if col_next.button("Next \u2192", type="primary", key="resume_next"):
        parsed = st.session_state.get("_parsed_resume", {})
        experience = (
            parsed.get("experience") or
            st.session_state.get("_experience", [])
        )
        errs = validate({"experience": experience})
        if errs:
            st.error("\n".join(errs))
        else:
            resume_yaml_path = CONFIG_DIR / "plain_text_resume.yaml"
            resume_yaml_path.parent.mkdir(parents=True, exist_ok=True)
            resume_data = {**parsed, "experience": experience} if parsed else {"experience": experience}
            resume_yaml_path.write_text(
                yaml.dump(resume_data, default_flow_style=False, allow_unicode=True)
            )
            _save_yaml({"wizard_step": 3})
            st.session_state.wizard_step = 4
            st.rerun()


# ── Step 4: Identity ───────────────────────────────────────────────────────────
elif step == 4:
    from app.wizard.step_identity import validate

    st.subheader("Step 4 \u2014 Your Identity")
    st.caption("Used in cover letter PDFs, LLM prompts, and the app header.")

    c1, c2 = st.columns(2)
    _parsed = st.session_state.get("_parsed_resume", {})
    name     = c1.text_input("Full Name *",  saved_yaml.get("name") or _parsed.get("name", ""))
    email    = c1.text_input("Email *",      saved_yaml.get("email") or _parsed.get("email", ""))
    phone    = c2.text_input("Phone",        saved_yaml.get("phone") or _parsed.get("phone", ""))
    linkedin = c2.text_input("LinkedIn URL", saved_yaml.get("linkedin", ""))

    # Career summary with optional LLM generation — resume text available now (step 3 ran first)
    summary_default = st.session_state.get("_gen_result_career_summary") or saved_yaml.get("career_summary", "")
    summary = st.text_area(
        "Career Summary *", value=summary_default, height=120,
        placeholder="Experienced professional with X years in [field]. Specialise in [skills].",
        help="Injected into cover letter and research prompts as your professional context.",
    )

    gen_result = _generation_widget(
        section="career_summary",
        label="Generate from resume",
        tier=_tier,
        feature_key="llm_career_summary",
        input_data={"resume_text": saved_yaml.get("_raw_resume_text", "")},
    )
    if gen_result and gen_result != summary:
        st.info(f"\u2728 Suggested summary \u2014 paste it above if it looks good:\n\n{gen_result}")

    col_back, col_next = st.columns([1, 4])
    if col_back.button("\u2190 Back", key="ident_back"):
        st.session_state.wizard_step = 3
        st.rerun()
    if col_next.button("Next \u2192", type="primary", key="ident_next"):
        errs = validate({"name": name, "email": email, "career_summary": summary})
        if errs:
            st.error("\n".join(errs))
        else:
            _save_yaml({
                "name": name, "email": email, "phone": phone,
                "linkedin": linkedin, "career_summary": summary,
                "wizard_complete": False, "wizard_step": 4,
            })
            st.session_state.wizard_step = 5
            st.rerun()


# ── Step 5: Inference ──────────────────────────────────────────────────────────
elif step == 5:
    from app.cloud_session import CLOUD_MODE as _CLOUD_MODE
    if _CLOUD_MODE:
        # Cloud deployment: inference is managed server-side; skip this step
        _save_yaml({"wizard_step": 5})
        st.session_state.wizard_step = 6
        st.rerun()

    from app.wizard.step_inference import validate

    st.subheader("Step 5 \u2014 Inference & API Keys")
    st.info(
        "**Simplest setup:** set `OLLAMA_HOST` in your `.env` file — "
        "Peregrine auto-detects it, no config file needed. "
        "Or use the fields below to configure API keys and endpoints."
    )
    profile = saved_yaml.get("inference_profile", "remote")

    if profile == "remote":
        st.info("Remote mode: at least one external API key is required.")
        anthropic_key = st.text_input("Anthropic API Key", type="password", placeholder="sk-ant-\u2026")
        openai_url    = st.text_input("OpenAI-compatible endpoint (optional)",
                                       placeholder="https://api.together.xyz/v1")
        openai_key    = st.text_input("Endpoint API Key (optional)", type="password",
                                       key="oai_key") if openai_url else ""
        ollama_host   = st.text_input("Ollama host (optional \u2014 local fallback)",
                                       placeholder="http://localhost:11434",
                                       key="ollama_host_input")
        ollama_model  = st.text_input("Ollama model (optional)",
                                       value="llama3.2:3b",
                                       key="ollama_model_input")
    else:
        st.info(f"Local mode ({profile}): Ollama provides inference.")
        import os
        _ollama_host_env = os.environ.get("OLLAMA_HOST", "")
        if _ollama_host_env:
            st.caption(f"OLLAMA_HOST from .env: `{_ollama_host_env}`")
        anthropic_key = openai_url = openai_key = ""

    with st.expander("Advanced \u2014 Service Ports & Hosts"):
        st.caption("Change only if services run on non-default ports or remote hosts.")
        svc = dict(saved_yaml.get("services", {}))
        for svc_name, default_host, default_port in [
            ("ollama",  "ollama",   11434),  # Docker service name
            ("vllm",    "vllm",     8000),   # Docker service name
            ("searxng", "searxng",  8080),   # Docker internal port (host-mapped: 8888)
        ]:
            c1, c2 = st.columns([3, 1])
            svc[f"{svc_name}_host"] = c1.text_input(
                f"{svc_name} host",
                svc.get(f"{svc_name}_host", default_host),
                key=f"h_{svc_name}",
            )
            svc[f"{svc_name}_port"] = int(c2.number_input(
                "port",
                value=int(svc.get(f"{svc_name}_port", default_port)),
                step=1, key=f"p_{svc_name}",
            ))

    confirmed = st.session_state.get("_inf_confirmed", False)
    test_label = "\U0001f50c Test Ollama connection" if profile != "remote" else "\U0001f50c Test LLM connection"
    if st.button(test_label, key="inf_test"):
        if profile == "remote":
            from scripts.llm_router import LLMRouter
            try:
                r = LLMRouter().complete("Reply with only: OK")
                if r and r.strip():
                    st.success("LLM responding.")
                    st.session_state["_inf_confirmed"] = True
                    confirmed = True
            except Exception as e:
                st.error(f"LLM test failed: {e}")
        else:
            import requests
            ollama_url = f"http://{svc.get('ollama_host','localhost')}:{svc.get('ollama_port',11434)}"
            try:
                requests.get(f"{ollama_url}/api/tags", timeout=5)
                st.success("Ollama is running.")
                st.session_state["_inf_confirmed"] = True
                confirmed = True
            except Exception:
                st.warning("Ollama not responding \u2014 you can skip this check and configure later.")
                st.session_state["_inf_confirmed"] = True
                confirmed = True

    col_back, col_next = st.columns([1, 4])
    if col_back.button("\u2190 Back", key="inf_back"):
        st.session_state.wizard_step = 4
        st.rerun()
    if col_next.button("Next \u2192", type="primary", key="inf_next", disabled=not confirmed):
        errs = validate({"endpoint_confirmed": confirmed})
        if errs:
            st.error("\n".join(errs))
        else:
            # Write API keys to .env
            env_path = _ROOT / ".env"
            env_lines = env_path.read_text().splitlines() if env_path.exists() else []

            def _set_env(lines: list[str], key: str, val: str) -> list[str]:
                for i, l in enumerate(lines):
                    if l.startswith(f"{key}="):
                        lines[i] = f"{key}={val}"
                        return lines
                lines.append(f"{key}={val}")
                return lines

            if anthropic_key:
                env_lines = _set_env(env_lines, "ANTHROPIC_API_KEY", anthropic_key)
            if openai_url:
                env_lines = _set_env(env_lines, "OPENAI_COMPAT_URL", openai_url)
            if openai_key:
                env_lines = _set_env(env_lines, "OPENAI_COMPAT_KEY", openai_key)
            if anthropic_key or openai_url:
                env_path.write_text("\n".join(env_lines) + "\n")

            if profile == "remote":
                if ollama_host:
                    env_lines = _set_env(env_lines, "OLLAMA_HOST", ollama_host)
                if ollama_model:
                    env_lines = _set_env(env_lines, "OLLAMA_MODEL", ollama_model)
                if ollama_host or ollama_model:
                    env_path.write_text("\n".join(env_lines) + "\n")

            _save_yaml({"services": svc, "wizard_step": 5})
            st.session_state.wizard_step = 6
            st.rerun()


# ── Step 6: Search ─────────────────────────────────────────────────────────────
elif step == 6:
    from app.wizard.step_search import validate

    st.subheader("Step 6 \u2014 Job Search Preferences")
    st.caption("Set up what to search for. You can refine these in Settings \u2192 Search later.")

    titles    = st.session_state.get("_titles",    saved_yaml.get("_wiz_titles", []))
    locations = st.session_state.get("_locations", saved_yaml.get("_wiz_locations", []))

    c1, c2 = st.columns(2)

    with c1:
        st.markdown("**Job Titles**")
        for i, t in enumerate(titles):
            tc1, tc2 = st.columns([5, 1])
            tc1.text(t)
            if tc2.button("\u00d7", key=f"rmtitle_{i}"):
                titles.pop(i)
                st.session_state["_titles"] = titles
                st.rerun()
        new_title = st.text_input("Add title", key="new_title_wiz",
                                   placeholder="Software Engineer, Product Manager\u2026")
        ac1, ac2 = st.columns([4, 1])
        if ac2.button("\uff0b", key="add_title"):
            if new_title.strip() and new_title.strip() not in titles:
                titles.append(new_title.strip())
                st.session_state["_titles"] = titles
                st.rerun()

        # LLM title suggestions
        _generation_widget(
            section="job_titles",
            label="Suggest job titles",
            tier=_tier,
            feature_key="llm_job_titles",
            input_data={
                "resume_text": saved_yaml.get("_raw_resume_text", ""),
                "current_titles": str(titles),
            },
        )

    with c2:
        st.markdown("**Locations**")
        for i, l in enumerate(locations):
            lc1, lc2 = st.columns([5, 1])
            lc1.text(l)
            if lc2.button("\u00d7", key=f"rmloc_{i}"):
                locations.pop(i)
                st.session_state["_locations"] = locations
                st.rerun()
        new_loc = st.text_input("Add location", key="new_loc_wiz",
                                 placeholder="Remote, New York NY, San Francisco CA\u2026")
        ll1, ll2 = st.columns([4, 1])
        if ll2.button("\uff0b", key="add_loc"):
            if new_loc.strip():
                locations.append(new_loc.strip())
                st.session_state["_locations"] = locations
                st.rerun()

    col_back, col_next = st.columns([1, 4])
    if col_back.button("\u2190 Back", key="search_back"):
        st.session_state.wizard_step = 5
        st.rerun()
    if col_next.button("Next \u2192", type="primary", key="search_next"):
        errs = validate({"job_titles": titles, "locations": locations})
        if errs:
            st.error("\n".join(errs))
        else:
            search_profile_path = CONFIG_DIR / "search_profiles.yaml"
            existing_profiles = {}
            if search_profile_path.exists():
                existing_profiles = yaml.safe_load(search_profile_path.read_text()) or {}
            profiles_list = existing_profiles.get("profiles", [])
            # Update or create "default" profile
            default_idx = next(
                (i for i, p in enumerate(profiles_list) if p.get("name") == "default"), None
            )
            default_profile = {
                "name": "default",
                "titles": titles,
                "locations": locations,
                "remote_only": False,
                "boards": ["linkedin", "indeed", "glassdoor", "zip_recruiter"],
            }
            if default_idx is not None:
                profiles_list[default_idx] = default_profile
            else:
                profiles_list.insert(0, default_profile)
            search_profile_path.write_text(
                yaml.dump({"profiles": profiles_list},
                          default_flow_style=False, allow_unicode=True)
            )
            _save_yaml({"wizard_step": 6})
            st.session_state.wizard_step = 7
            st.rerun()


# ── Step 7: Integrations (optional) ───────────────────────────────────────────
elif step == 7:
    st.subheader("Step 7 \u2014 Integrations (Optional)")
    st.caption(
        "Connect cloud services, calendars, and notification tools. "
        "You can add or change these any time in Settings \u2192 Integrations."
    )

    from scripts.integrations import REGISTRY
    from app.wizard.step_integrations import get_available, is_connected
    from app.wizard.tiers import tier_label

    available = get_available(_tier)

    for name, cls in sorted(REGISTRY.items(), key=lambda x: (x[0] not in available, x[0])):
        is_conn = is_connected(name, CONFIG_DIR)
        icon    = "\u2705" if is_conn else "\u25cb"
        lock    = tier_label(f"{name}_sync") or tier_label(f"{name}_notifications")

        with st.expander(f"{icon} {cls.label}  {lock}"):
            if name not in available:
                st.caption(f"Upgrade to {cls.tier} to unlock {cls.label}.")
                continue

            inst   = cls()
            config: dict = {}
            for field in inst.fields():
                val = st.text_input(
                    field["label"],
                    type="password" if field["type"] == "password" else "default",
                    placeholder=field.get("placeholder", ""),
                    help=field.get("help", ""),
                    key=f"int_{name}_{field['key']}",
                )
                config[field["key"]] = val

            required_filled = all(
                config.get(f["key"])
                for f in inst.fields()
                if f.get("required")
            )
            if st.button(f"Connect {cls.label}", key=f"conn_{name}",
                          disabled=not required_filled):
                inst.connect(config)
                with st.spinner(f"Testing {cls.label} connection\u2026"):
                    if inst.test():
                        inst.save_config(config, CONFIG_DIR)
                        st.success(f"{cls.label} connected!")
                        st.rerun()
                    else:
                        st.error(
                            f"Connection test failed for {cls.label}. "
                            "Double-check your credentials."
                        )

    st.divider()
    col_back, col_skip, col_finish = st.columns([1, 1, 3])

    if col_back.button("\u2190 Back", key="int_back"):
        st.session_state.wizard_step = 6
        st.rerun()

    if col_skip.button("Skip \u2192"):
        st.session_state.wizard_step = 8  # trigger Finish
        st.rerun()

    if col_finish.button("\U0001f389 Finish Setup", type="primary", key="finish_btn"):
        st.session_state.wizard_step = 8
        st.rerun()


# ── Finish ─────────────────────────────────────────────────────────────────────
elif step >= 8:
    with st.spinner("Finalising setup\u2026"):
        from scripts.user_profile import UserProfile
        from scripts.generate_llm_config import apply_service_urls

        try:
            profile_obj = UserProfile(USER_YAML)
            if (CONFIG_DIR / "llm.yaml").exists():
                apply_service_urls(profile_obj, CONFIG_DIR / "llm.yaml")
        except Exception:
            pass  # don't block finish on llm.yaml errors

        data = _load_yaml()
        data["wizard_complete"] = True
        data.pop("wizard_step", None)
        USER_YAML.write_text(
            yaml.dump(data, default_flow_style=False, allow_unicode=True)
        )

    st.success("\u2705 Setup complete! Loading Peregrine\u2026")
    st.session_state.clear()
    st.rerun()
