"""Onboarding conversation endpoint.

Runs a guided conversation to generate personalized PROFILE.md and CONTRACT.md.
"""

from __future__ import annotations

import logging
from pathlib import Path

from fastapi import APIRouter, Request
from pydantic import BaseModel

router = APIRouter(prefix="/onboarding", tags=["onboarding"])

log = logging.getLogger(__name__)

_ONBOARDING_SYSTEM = """You are the AgntSpace onboarding assistant. Your job is to learn about the user through a friendly, concise conversation and generate their personalized PROFILE.md and CONTRACT.md.

## Conversation Flow

Ask questions ONE AT A TIME. Keep it conversational, not like a form. Cover these areas:

1. **Name and role** — "What should I call you? What's your primary role?"
2. **Companies/projects** — "What companies or projects are you working on?"
3. **Tools** — "What tools do you use daily? (email, calendar, messaging, notes)"
4. **Communication style** — "How do you prefer to communicate? Concise? Detailed?"
5. **What to automate** — "What tasks eat up your time that you'd love to hand off?"
6. **Proactivity level** — Explain the 4 tiers (Observer/Advisor/Assistant/Partner) and ask which fits
7. **Privacy preference** — "Cloud LLM for quality, or local for full privacy?"
8. **Sensitive areas** — "Any topics I should always ask before accessing? (finances, health, etc.)"
9. **Timezone and work hours** — "What timezone and work hours?"

After gathering enough info (usually 5-8 exchanges), say EXACTLY:

```
ONBOARDING_COMPLETE
```

Followed by two fenced code blocks:

```profile
(the full PROFILE.md content in markdown with YAML frontmatter)
```

```contract
(the full CONTRACT.md content in markdown with YAML frontmatter)
```

Use the default PROFILE.md and CONTRACT.md templates as the base structure, but fill them in with the user's actual answers. For CONTRACT.md, set the proactivity level, privacy mode, and sensitive directories based on their answers.

Be warm but efficient. Don't ask more than one question per message. Don't be robotic — this should feel like meeting a new colleague."""


class OnboardingRequest(BaseModel):
    message: str
    conversation_id: str | None = None


class OnboardingResponse(BaseModel):
    message: str
    conversation_id: str
    complete: bool
    profile: str | None = None
    contract: str | None = None


# ────────────────────────────────────────────────────────────────────
# Form-based onboarding — deterministic, no LLM, instant.
# ────────────────────────────────────────────────────────────────────

class OnboardingFormRequest(BaseModel):
    """Structured form data from the deterministic onboarding flow."""
    name: str
    role: str = ""
    company: str = ""
    communication_style: str = "concise"  # concise | detailed | balanced
    timezone: str = ""
    work_hours: str = ""
    proactivity: str = "advisor"  # observer | advisor | assistant | partner
    privacy_mode: str = "cloud"  # cloud | local | both


@router.get("/prefill")
async def onboarding_prefill(request: Request) -> dict:
    """Return existing profile/contract data to pre-fill the form.

    Parses PROFILE.md and CONTRACT.md back into structured fields so the
    onboarding form isn't blank when the user re-runs it.
    """
    vault = request.app.state.vault
    identity = vault.read_identity_files()
    result: dict = {}

    profile = identity.get("PROFILE")
    if profile and profile.content:
        text = profile.content
        fm = profile.metadata or {}

        # Name: try USER.md first (canonical)
        user_file = identity.get("USER")
        if user_file and user_file.content:
            for line in user_file.content.splitlines():
                if "**Name**:" in line:
                    val = line.split("**Name**:", 1)[1].strip()
                    if val and "[" not in val:
                        result["name"] = val
                        break

        # Parse structured fields from USER.md (they live there, not in PROFILE)
        source = user_file.content if (user_file and user_file.content) else ""
        for line in source.splitlines():
            stripped = line.strip()
            if "**Role**:" in stripped:
                val = stripped.split("**Role**:", 1)[1].strip()
                if val and "[" not in val:
                    result["role"] = val
            elif "**Timezone**:" in stripped:
                val = stripped.split("**Timezone**:", 1)[1].strip()
                if val and "[" not in val:
                    result["timezone"] = val
            elif "**Work hours**:" in stripped:
                val = stripped.split("**Work hours**:", 1)[1].strip()
                if val and "[" not in val:
                    result["work_hours"] = val
            elif "**Companies**:" in stripped:
                val = stripped.split("**Companies**:", 1)[1].strip()
                if val and "[" not in val:
                    result["company"] = val

        # Also check PROFILE.md for fields (fallback for older profiles)
        for line in text.splitlines():
            stripped = line.strip()
            if "role" not in result and stripped.startswith("Role:"):
                result["role"] = stripped.split(":", 1)[1].strip()
            elif "company" not in result and stripped.startswith("Company") and ":" in stripped:
                result["company"] = stripped.split(":", 1)[1].strip()
            elif "timezone" not in result and stripped.startswith("Timezone:"):
                result["timezone"] = stripped.split(":", 1)[1].strip()
            elif "work_hours" not in result and stripped.startswith("Work hours:"):
                result["work_hours"] = stripped.split(":", 1)[1].strip()

        # Communication style — match known phrases
        lower = text.lower()
        if "concise" in lower:
            result["communication_style"] = "concise"
        elif "detailed" in lower:
            result["communication_style"] = "detailed"
        elif "balanced" in lower:
            result["communication_style"] = "balanced"

    contract = identity.get("CONTRACT")
    if contract and contract.metadata:
        fm = contract.metadata
        if "proactivity" in fm:
            result["proactivity"] = fm["proactivity"]
        if "privacy_mode" in fm:
            result["privacy_mode"] = fm["privacy_mode"]

    return result


@router.get("/status")
async def onboarding_status(request: Request) -> dict:
    """Check if onboarding has been completed."""
    vault = request.app.state.vault
    identity = vault.read_identity_files()
    profile = identity.get("PROFILE")
    onboarded = (
        profile is not None
        and "[Your name]" not in profile.content
        and len(profile.content.strip()) > 200
    )
    return {"onboarded": onboarded}


@router.post("/chat", response_model=OnboardingResponse)
async def onboarding_chat(request: Request, body: OnboardingRequest) -> OnboardingResponse:
    """Chat with the onboarding assistant."""
    agent = request.app.state.agent
    memory = request.app.state.memory
    llm = agent._llm  # noqa: SLF001

    conv_id = body.conversation_id
    if not conv_id or not await memory.conversation_exists(conv_id):
        conv_id = await memory.create_conversation(title="Onboarding")

    # Store user message
    await memory.add_message(conv_id, "user", body.message)
    history = await memory.get_context_messages(conv_id)

    # Use onboarding system prompt instead of the agent's identity prompt
    from agntspace.llm.base import Message

    msgs = [Message(role=m.role, content=m.content) for m in history]
    response = await llm.complete(
        messages=msgs,
        system=_ONBOARDING_SYSTEM,
        max_tokens=4096,
    )

    reply = response.content
    await memory.add_message(conv_id, "assistant", reply)

    # Check if onboarding is complete
    complete = "ONBOARDING_COMPLETE" in reply
    profile_content = None
    contract_content = None

    if complete:
        profile_content = _extract_block(reply, "profile")
        contract_content = _extract_block(reply, "contract")

        # Save to vault — only if LLM produced complete files (with YAML block
        # for contract).  Never overwrite if the generated content is incomplete,
        # because that would destroy the default template.
        if profile_content or contract_content:
            from agntspace.vault.writer import VaultWriter

            settings = request.app.state.settings
            writer = VaultWriter(settings.vault_dir)

            if profile_content and len(profile_content) > 200:
                writer.write("system/identity/PROFILE.md", profile_content)
                log.info("Onboarding: saved system/identity/PROFILE.md")
            elif profile_content:
                log.warning("Onboarding: LLM profile too short (%d chars), keeping default", len(profile_content))

            if contract_content and "```yaml" in contract_content:
                writer.write("system/identity/CONTRACT.md", contract_content)
                log.info("Onboarding: saved system/identity/CONTRACT.md")
            elif contract_content:
                log.warning("Onboarding: LLM contract missing YAML block, keeping default")

            # Reinitialize agent with new identity
            await agent.initialize()
            log.info("Onboarding: agent reinitialized with new identity")

        # Clean the reply for display (remove the raw blocks)
        display_reply = reply.split("ONBOARDING_COMPLETE")[0].strip()
        if not display_reply:
            display_reply = "Onboarding complete! Your profile and contract have been set up. Head to the dashboard to start using AgntSpace."
    else:
        display_reply = reply

    return OnboardingResponse(
        message=display_reply,
        conversation_id=conv_id,
        complete=complete,
        profile=profile_content,
        contract=contract_content,
    )


def _extract_block(text: str, label: str) -> str | None:
    """Extract content from a fenced code block with a given label."""
    marker = f"```{label}"
    start = text.find(marker)
    if start == -1:
        return None
    start = text.index("\n", start) + 1
    end = text.find("```", start)
    if end == -1:
        return text[start:].strip()
    return text[start:end].strip()


# ────────────────────────────────────────────────────────────────────
# Quick-start path — bypasses the conversational onboarding entirely.
# ────────────────────────────────────────────────────────────────────
# Quick-start and form-based onboarding both use the patch-in-place pattern:
# init.py copies the complete default templates (CONTRACT.md, PROFILE.md, etc.)
# from defaults/vault/ into the vault.  Onboarding then patches only the fields
# the user provided — proactivity, privacy_mode, name, role, etc.  The full
# template structure (YAML block, journal settings, trust, coaching, rights,
# security sections, placeholder tables) is NEVER replaced.
#
# The old _QUICK_CONTRACT / _QUICK_PROFILE / _build_contract / _build_profile
# functions have been removed — they overwrote the 266-line default with a
# 30-line stub, destroying all structure.  See _patch_contract / _patch_profile.


@router.post("/form")
async def onboarding_form(request: Request, body: OnboardingFormRequest) -> dict:
    """Deterministic form-based onboarding — no LLM, instant.

    Generates PROFILE.md and CONTRACT.md from structured form inputs using
    templates.  Zero round trips, can never get stuck.
    """
    from datetime import date

    from agntspace.vault.writer import VaultWriter

    settings = request.app.state.settings
    writer = VaultWriter(settings.vault_dir)

    today = date.today().isoformat()
    vault = request.app.state.vault

    # Patch in place — never overwrite the complete default templates
    _patch_profile(vault, writer, body, today)
    _patch_contract(vault, writer, body, today)
    _patch_user_md(writer, vault, body)

    log.info("Form onboarding: PROFILE + CONTRACT + USER patched for %s", body.name)

    # Persist timezone to config.toml + reconfigure runtime
    if body.timezone:
        try:
            svc = request.app.state.settings_service
            svc.set_value("schedule", "timezone", body.timezone)
            from agntspace.infra.timezone import configure as tz_configure
            tz_configure(body.timezone)
        except Exception:
            log.warning("Onboarding: failed to save timezone to config (non-fatal)")

    try:
        agent = request.app.state.agent
        await agent.initialize()
    except Exception:
        log.exception("Form onboarding: agent reinitialize failed (non-fatal)")

    return {"ok": True}


def _patch_user_md(writer, vault, form: OnboardingFormRequest) -> None:
    """Update USER.md in-place, replacing placeholder values with form data.

    USER.md is the canonical identity file — channels, local-mode prompts,
    and the system prompt all read Name/Role/Timezone from it.  We do a
    line-level replacement so custom edits the user made elsewhere in the
    file are preserved.
    """
    identity = vault.read_identity_files()
    user_file = identity.get("USER")
    if not user_file or not user_file.content:
        return

    lines = user_file.content.splitlines()
    replacements = {
        "**Name**": form.name,
        "**Role**": form.role,
        "**Timezone**": form.timezone,
        "**Location**": form.timezone.split("/", 1)[-1].replace("_", " ") if "/" in form.timezone else "",
        "**Work hours**": form.work_hours,
        "**Companies**": form.company,
    }

    # Communication style → Tone field
    tone_map = {
        "concise": "Direct, concise, no fluff",
        "detailed": "Thorough and detailed, with context and reasoning",
        "balanced": "Balanced — concise by default, detailed when needed",
    }
    if form.communication_style in tone_map:
        replacements["**Tone**"] = tone_map[form.communication_style]

    # Proactivity
    replacements["**Desired proactivity**"] = form.proactivity.capitalize()

    new_lines = []
    for line in lines:
        replaced = False
        for key, value in replacements.items():
            if key in line and value:
                # Replace everything after the key+colon
                prefix = line[: line.index(key) + len(key)]
                new_lines.append(f"{prefix}: {value}")
                replaced = True
                break
        if not replaced:
            new_lines.append(line)

    writer.write("system/identity/USER.md", "\n".join(new_lines) + "\n", trust_reason="onboarding_form")


def _patch_profile(reader, writer, form: OnboardingFormRequest, today: str) -> None:
    """Patch PROFILE.md in place — fill user-provided fields, leave template intact.

    Never replaces the file wholesale.  The complete default template (with 8
    sections, placeholders, tables) is preserved.  Onboarding only fills in
    fields the user actually provided.
    """
    import re

    try:
        doc = reader.read("system/identity/PROFILE.md")
        content = doc.content
    except Exception:
        return

    # Update frontmatter date
    content = re.sub(r"(?m)^updated:\s*.+$", f"updated: {today}", content)

    # Patch About You section fields
    if form.name:
        content = re.sub(r"(?m)^\- \*\*Name\*\*:\s*\[.*?\]", f"- **Name**: {form.name}", content)
    if form.role:
        content = re.sub(r"(?m)^\- \*\*Primary role\*\*:\s*\[.*?\]", f"- **Primary role**: {form.role}", content)
    if form.company:
        content = re.sub(r"(?m)^\- \*\*Other roles\*\*:\s*\[.*?\]", f"- **Other roles**: {form.company}", content)

    # Patch Proactivity Preferences
    if form.proactivity:
        content = re.sub(
            r"(?m)^\- \*\*Desired level\*\*:\s*\[.*?\]",
            f"- **Desired level**: {form.proactivity.capitalize()}",
            content,
        )

    # Patch Quiet Hours from work_hours
    if form.work_hours:
        content = re.sub(
            r"(?m)^\- \*\*Quiet hours\*\*:\s*\[.*?\]",
            f"- **Quiet hours**: outside {form.work_hours}",
            content,
        )

    writer.write("system/identity/PROFILE.md", content, trust_reason="onboarding")


def _apply_tier_permissions(content: str, proactivity: str) -> str:
    """Adjust the YAML permissions block based on the chosen proactivity tier.

    Loads tier config from ``_meta/onboarding/config/contract-tiers.yaml``.
    Actions listed under ``promote_to_allow`` move from confirm→allow.
    Actions listed under ``demote_to_confirm`` move from allow→confirm.
    The blocked-forever list is never touched.

    Falls back to unchanged content if the config is missing or the tier
    has no adjustments (advisor = the default template).
    """
    import re

    import yaml

    # Load tier config — try vault skills, then bundled defaults
    tier_config: dict = {}
    for base in [
        Path(__file__).resolve().parents[5] / "defaults",  # repo root
    ]:
        cfg = base / "skills" / "_meta" / "onboarding" / "config" / "contract-tiers.yaml"
        if cfg.exists():
            try:
                tier_config = yaml.safe_load(cfg.read_text(encoding="utf-8")) or {}
            except Exception:
                pass
            break

    tier = tier_config.get(proactivity, {})
    promote_to_allow = set(tier.get("promote_to_allow", []) or [])
    demote_to_confirm = set(tier.get("demote_to_confirm", []) or [])

    if not promote_to_allow and not demote_to_confirm:
        return content  # advisor or empty config — no changes

    # Extract the YAML block
    m = re.search(r"```yaml\s*\n(.*?)```", content, flags=re.DOTALL)
    if not m:
        return content

    yaml_text = m.group(1)
    try:
        rules = yaml.safe_load(yaml_text) or {}
    except Exception:
        return content

    allow = set(rules.get("allow", []) or [])
    confirm = set(rules.get("confirm", []) or [])
    block = set(rules.get("block", []) or [])

    # Apply promotions: confirm → allow
    for action in promote_to_allow:
        if action in confirm:
            confirm.discard(action)
            allow.add(action)

    # Apply demotions: allow → confirm
    for action in demote_to_confirm:
        if action in allow:
            allow.discard(action)
            confirm.add(action)

    # Rebuild YAML text preserving the other fields (default, overrides, gated_dirs)
    lines = [f"default: {rules.get('default', 'deny')}"]
    lines.append("")
    lines.append("allow:")
    for a in sorted(allow):
        lines.append(f"  - {a}")
    lines.append("")
    lines.append("confirm:")
    for a in sorted(confirm):
        lines.append(f"  - {a}")
    lines.append("")
    lines.append("block:")
    for a in sorted(block):
        lines.append(f"  - {a}")

    # Preserve overrides and gated_dirs
    overrides = rules.get("overrides") or {}
    lines.append("")
    lines.append("overrides:")
    if overrides and isinstance(overrides, dict):
        for k, v in overrides.items():
            lines.append(f"  {k}: {v}")
    else:
        lines.append("  # Per-integration overrides (uncomment and edit as needed):")

    gated = rules.get("gated_dirs") or []
    lines.append("")
    lines.append("gated_dirs:")
    for d in gated:
        lines.append(f"  - {d}")

    new_yaml = "\n".join(lines)
    content = content[:m.start(1)] + new_yaml + "\n" + content[m.end(1):]
    return content


def _patch_contract(reader, writer, form: OnboardingFormRequest, today: str) -> None:
    """Patch CONTRACT.md in place — update only frontmatter + prose sections the user chose.

    Never replaces the file wholesale.  The complete default template (with YAML
    permissions block, journal settings, trust, coaching, etc.) is preserved.
    Onboarding only touches: frontmatter proactivity/privacy_mode, the
    Proactivity prose section, and the Privacy prose section.
    """
    import re

    proactivity = form.proactivity if form.proactivity in ("observer", "advisor", "assistant", "partner") else "advisor"
    privacy = form.privacy_mode if form.privacy_mode in ("cloud", "local", "both") else "cloud"

    tier_desc = {
        "observer": "Observer — the agent provides information only when asked. No proactive suggestions.",
        "advisor": "Advisor — the agent offers suggestions and reminders but does not execute without permission.",
        "assistant": "Assistant — the agent takes initiative on routine tasks and asks for approval on the rest.",
        "partner": "Partner — the agent manages areas independently within the boundaries of this contract.",
    }

    # Read the existing file (init.py already copied the complete default)
    try:
        doc = reader.read("system/identity/CONTRACT.md")
        content = doc.content
    except Exception:
        # Fallback: if file doesn't exist yet, nothing to patch
        return

    # Patch frontmatter fields — add if missing (default template lacks them)
    if re.search(r"(?m)^proactivity:", content):
        content = re.sub(r"(?m)^proactivity:\s*\S+", f"proactivity: {proactivity}", content)
    else:
        content = re.sub(r"(?m)^(---\s*\n)", rf"\g<1>proactivity: {proactivity}\n", content, count=1)

    if re.search(r"(?m)^privacy_mode:", content):
        content = re.sub(r"(?m)^privacy_mode:\s*\S+", f"privacy_mode: {privacy}", content)
    else:
        content = re.sub(r"(?m)^(---\s*\n)", rf"\g<1>privacy_mode: {privacy}\n", content, count=1)

    content = re.sub(r"(?m)^((?:date|updated):\s*).+$", rf"\g<1>{today}", content)

    # Patch the Proactivity prose line (matches **Level**: ... or **Tier — ...)
    content = re.sub(
        r"(?m)^\*\*(?:Level|Observer|Advisor|Assistant|Partner)\b[^\n]*$",
        f"**{tier_desc[proactivity]}**",
        content,
        count=1,
    )

    # Patch the Privacy Mode prose line
    content = re.sub(
        r"(?m)^\*\*Mode\*\*:\s*\w+",
        f"**Mode**: {privacy}",
        content,
        count=1,
    )

    # Apply tier-aware permission defaults from contract-tiers.yaml
    content = _apply_tier_permissions(content, proactivity)

    writer.write("system/identity/CONTRACT.md", content, trust_reason="onboarding")


@router.post("/quick-start")
async def onboarding_quick_start(request: Request) -> dict:
    """Skip the conversational onboarding and install a baseline identity.

    Writes a real (not placeholder) PROFILE.md and CONTRACT.md, reloads the
    agent so the new identity takes effect immediately, and returns ok.
    Returns the same response shape as the chat flow's terminal state so
    the UI can treat both paths uniformly.
    """
    from datetime import date

    from agntspace.vault.writer import VaultWriter

    settings = request.app.state.settings
    writer = VaultWriter(settings.vault_dir)

    today = date.today().isoformat()
    vault = request.app.state.vault

    # Quick-start: patch only frontmatter dates + advisor/cloud defaults.
    # The complete default templates stay intact.
    _patch_contract(vault, writer, OnboardingFormRequest(
        name="", proactivity="advisor", privacy_mode="cloud",
    ), today)
    # PROFILE.md — just update the date, no user data to fill
    import re
    try:
        doc = vault.read("system/identity/PROFILE.md")
        content = re.sub(r"(?m)^updated:\s*.+$", f"updated: {today}", doc.content)
        writer.write("system/identity/PROFILE.md", content, trust_reason="onboarding_quickstart")
    except Exception:
        pass  # file exists with defaults, that's fine

    log.info("Quick-start onboarding: PROFILE + CONTRACT patched with defaults")

    # Reinitialize the agent so the new identity is reflected in the next
    # chat turn without requiring a server restart.
    try:
        agent = request.app.state.agent
        await agent.initialize()
    except Exception:
        log.exception("Quick-start: agent reinitialize failed (non-fatal)")

    return {
        "ok": True,
        "profile_path": "system/identity/PROFILE.md",
        "contract_path": "system/identity/CONTRACT.md",
    }
