# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Onboarding routes — /api/onboard/* (first-run, localhost only)."""

from __future__ import annotations

import json
import logging
import os
import queue
import threading

from flask import Blueprint, Response, jsonify, render_template, request, send_file

from ..auth import require_auth, require_localhost
from ..helpers import get_agent, get_service_manager, get_wizard_host, AGENT_DIR, DATA_DIR

logger = logging.getLogger(__name__)

bp = Blueprint("onboard", __name__)




# ============================================================
# Onboarding Routes (no auth — first-run, localhost only)
# ============================================================

# Session-scoped engine instance for the onboarding flow
_onboard_engine = None
_onboard_lock = threading.Lock()
_activation_lock = threading.Lock()


def _get_onboard_engine():
    """Get or create the onboarding engine for this session."""
    global _onboard_engine
    with _onboard_lock:
        if _onboard_engine is None:
            from familiar.onboard_engine import OnboardingEngine

            _onboard_engine = OnboardingEngine()
    return _onboard_engine



@bp.route("/api/tutorial/complete", methods=["POST"])
@require_auth
def api_tutorial_complete():
    """Mark the dashboard tutorial as completed (server-side flag)."""
    flag = DATA_DIR / ".dashboard_tutorial_done"
    flag.parent.mkdir(parents=True, exist_ok=True)
    flag.write_text("1")
    return jsonify({"ok": True})


@bp.route("/api/tutorial/reset", methods=["POST"])
@require_auth
def api_tutorial_reset():
    """Reset the dashboard tutorial so it runs again on next load."""
    flag = DATA_DIR / ".dashboard_tutorial_done"
    try:
        flag.unlink(missing_ok=True)
    except OSError as e:
        logger.debug("I/O error suppressed: %s", e)
    return jsonify({"ok": True, "message": "Tutorial reset. Reload the dashboard to start it."})


@bp.route("/onboard")
@require_localhost
def onboard_page():
    """Serve the onboarding wizard UI."""
    return render_template("onboard.html")


@bp.route("/api/onboard/detect")
@require_localhost
def api_onboard_detect():
    """Detect available LLM providers."""
    engine = _get_onboard_engine()
    result = engine.detect_providers()
    return jsonify(result.data)


@bp.route("/api/onboard/creature-data")
@require_localhost
def api_onboard_creature_data():
    """Return species, palette, and constitution data for the onboarding wizard."""
    from familiar.onboard_engine import OnboardingEngine

    return jsonify(OnboardingEngine.get_creature_data())


@bp.route("/api/onboard/creature-preview", methods=["POST"])
@require_localhost
def api_onboard_creature_preview():
    """Render a creature sprite preview given species and colors."""
    data = request.json or {}
    species = data.get("species", "fox")
    color_body = data.get("color_body", "#4A90D9")
    color_eyes = data.get("color_eyes", "#FFD700")
    color_accent = data.get("color_accent", "#FF6B6B")

    from familiar.creature.palette import build_color_map
    from familiar.creature.renderer_html import render_frame_html
    from familiar.creature.sprites import EGG_FRAMES, get_sprite_frames

    if species == "egg":
        frames = EGG_FRAMES
        colors = build_color_map(color_body, color_eyes, color_accent)
    else:
        colors = build_color_map(color_body, color_eyes, color_accent)
        frames = get_sprite_frames(species, "baby", "idle")

    sprite_html = render_frame_html(frames[0], colors)
    return jsonify({"sprite_html": sprite_html})


@bp.route("/api/onboard/validate/<step>", methods=["POST"])
@require_localhost
def api_onboard_validate(step):
    """Validate a single onboarding step."""
    engine = _get_onboard_engine()
    data = request.json or {}

    validators = {
        "provider": engine.validate_provider,
        "channels": engine.validate_channels,
        "owner_pin": engine.validate_owner_pin,
        "identity": engine.validate_identity,
        "profile": engine.validate_profile,
        "credentials": engine.validate_credentials,
        "briefing": engine.validate_briefing,
        "encryption": engine.validate_encryption,
        "creature": engine.validate_creature,
    }

    validator = validators.get(step)
    if not validator:
        return jsonify({"success": False, "errors": [f"Unknown step: {step}"]}), 400

    result = validator(data)
    return jsonify(
        {
            "success": result.success,
            "errors": result.errors,
            "warnings": result.warnings,
            "data": result.data,
        }
    )


@bp.route("/api/onboard/ollama-bundles")
@require_localhost
def api_onboard_ollama_bundles():
    """Get RAM-based Ollama model bundle recommendations."""
    engine = _get_onboard_engine()
    bundles = engine.get_ollama_bundles()
    return jsonify(bundles)


@bp.route("/api/onboard/test-message", methods=["POST"])
@require_localhost
def api_onboard_test_message():
    """Send a test message after activation."""
    engine = _get_onboard_engine()
    result = engine.send_test_message()
    return jsonify(
        {
            "success": result.success,
            "warnings": result.warnings,
            "errors": result.errors,
        }
    )


@bp.route("/api/onboard/checklist")
@require_localhost
def api_onboard_checklist():
    """Get post-activation next-steps checklist."""
    engine = _get_onboard_engine()
    return jsonify(engine.get_post_activation_checklist())


@bp.route("/api/onboard/save-state", methods=["POST"])
@require_localhost
def api_onboard_save_state():
    """Persist wizard state server-side for cross-browser resume."""
    engine = _get_onboard_engine()
    data = request.json or {}
    step = data.get("step", 0)
    # Save JS UI state alongside engine state (don't overwrite engine._state)
    engine.save_state(step, ui_state=data.get("state"))
    return jsonify({"success": True})


@bp.route("/api/onboard/load-state")
@require_localhost
def api_onboard_load_state():
    """Load saved wizard state from server."""
    engine = _get_onboard_engine()
    saved = engine.load_saved_state()
    if saved:
        return jsonify(
            {
                "found": True,
                "step": saved.get("step", 0),
                "state": saved.get("ui_state", saved.get("state", {})),
            }
        )
    return jsonify({"found": False})


@bp.route("/api/onboard/credentials.pdf")
@require_localhost
def api_onboard_credentials_pdf():
    """Generate a PDF with dashboard URL and API key."""
    import io

    api_key = request.args.get("key", "")
    port = request.host.split(":")[-1] if ":" in request.host else "5000"
    dash_url = f"http://127.0.0.1:{port}?api_key={api_key}"

    # Minimal valid PDF with credentials
    lines = [
        "Familiar Dashboard Credentials",
        "",
        "Dashboard URL:",
        dash_url,
        "",
        "API Key:",
        api_key,
        "",
        "Keep this file safe. You can also find your key in:",
        "~/.familiar/.env (FAMILIAR_DASHBOARD_KEY)",
        "",
        "To start Familiar, run:",
        "  source ~/familiar-env/bin/activate",
        "  familiar",
    ]
    text = "\n".join(lines)

    # Build raw PDF (no dependencies needed)
    stream_content = "BT\n/F1 18 Tf\n50 740 Td\n(Familiar Dashboard Credentials) Tj\n"
    stream_content += "/F1 11 Tf\n0 -36 Td\n"
    y_items = [
        ("Dashboard URL:", ""),
        (dash_url, ""),
        ("", ""),
        ("API Key:", ""),
        (api_key, ""),
        ("", ""),
        ("Keep this file safe.", ""),
        ("Your key is also saved in: ~/.familiar/.env", ""),
        ("", ""),
        ("To start Familiar, run:", ""),
        ("  source ~/familiar-env/bin/activate && familiar", ""),
    ]
    for line_text, _ in y_items:
        escaped = line_text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
        stream_content += f"0 -16 Td\n({escaped}) Tj\n"
    stream_content += "ET"

    obj1 = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
    obj2 = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
    obj3 = (
        "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
        "/Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n"
    )
    obj4 = f"4 0 obj\n<< /Length {len(stream_content)} >>\nstream\n{stream_content}\nendstream\nendobj\n"
    obj5 = "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>\nendobj\n"

    body = "%PDF-1.4\n"
    offsets = []
    for obj in [obj1, obj2, obj3, obj4, obj5]:
        offsets.append(len(body))
        body += obj

    xref_offset = len(body)
    body += "xref\n"
    body += f"0 {len(offsets) + 1}\n"
    body += "0000000000 65535 f \n"
    for off in offsets:
        body += f"{off:010d} 00000 n \n"
    body += "trailer\n"
    body += f"<< /Size {len(offsets) + 1} /Root 1 0 R >>\n"
    body += "startxref\n"
    body += f"{xref_offset}\n"
    body += "%%EOF\n"

    buf = io.BytesIO(body.encode("latin-1"))
    return send_file(
        buf,
        mimetype="application/pdf",
        as_attachment=True,
        download_name="familiar-credentials.pdf",
    )


@bp.route("/api/onboard/clear-state", methods=["POST"])
@require_localhost
def api_onboard_clear_state():
    """Clear saved wizard state on the server."""
    engine = _get_onboard_engine()
    engine.clear_saved_state()
    return jsonify({"success": True})


@bp.route("/api/onboard/activate", methods=["POST"])
@require_localhost
def api_onboard_activate():
    """Activate the configuration (write config, install deps)."""
    # first_run_mode managed by auth module
    engine = _get_onboard_engine()
    result = engine.activate()
    # Reload .env so the running process picks up new keys
    env_path = AGENT_DIR / ".env"
    if env_path.exists():
        try:
            from dotenv import load_dotenv

            load_dotenv(str(env_path), override=True)
        except ImportError:
            pass
    if result.success:
        from familiar.dashboard.auth import set_first_run_mode; set_first_run_mode(False)
        # Set up auth key for the now-active dashboard
        key = os.environ.get("FAMILIAR_DASHBOARD_KEY")
        if not key:
            import secrets as _secrets

            key = _secrets.token_urlsafe(32)
            try:
                with open(env_path, "a") as f:
                    f.write(f"\nFAMILIAR_DASHBOARD_KEY={key}\n")
            except OSError:
                pass
        os.environ["FAMILIAR_DASHBOARD_KEY"] = key
        require_auth._auto_key = key
    return jsonify(
        {
            "success": result.success,
            "errors": result.errors,
            "warnings": result.warnings,
            "api_key": os.environ.get("FAMILIAR_DASHBOARD_KEY", ""),
        }
    )


@bp.route("/api/onboard/activate/stream")
@require_localhost
def api_onboard_activate_stream():
    """SSE stream of activation progress events."""
    if not _activation_lock.acquire(blocking=False):
        return jsonify({"error": "Activation already in progress"}), 409

    engine = _get_onboard_engine()
    msg_queue = queue.Queue()

    def _notify(msg):
        msg_queue.put(msg)

    def _run_activation():
        # first_run_mode managed by auth module
        try:
            result = engine.activate(notify=_notify)
            # Reload .env so the running process picks up new keys
            env_path = AGENT_DIR / ".env"
            if env_path.exists():
                try:
                    from dotenv import load_dotenv

                    load_dotenv(str(env_path), override=True)
                except ImportError:
                    pass
            if result.success:
                from familiar.dashboard.auth import set_first_run_mode; set_first_run_mode(False)
                # Set up auth key for the now-active dashboard
                key = os.environ.get("FAMILIAR_DASHBOARD_KEY")
                if not key:
                    import secrets as _secrets

                    key = _secrets.token_urlsafe(32)
                    try:
                        with open(env_path, "a") as f:
                            f.write(f"\nFAMILIAR_DASHBOARD_KEY={key}\n")
                    except OSError:
                        pass
                os.environ["FAMILIAR_DASHBOARD_KEY"] = key
                require_auth._auto_key = key
            msg_queue.put(("__DONE__", result.success, result.errors, result.data))
        finally:
            _activation_lock.release()

    # Run activation in background thread
    t = threading.Thread(target=_run_activation, daemon=True)
    t.start()

    def generate():
        progress = 10
        step_increment = 12  # ~8 steps, 12% each gets to ~100%
        while True:
            try:
                item = msg_queue.get(timeout=60)
            except queue.Empty:
                yield f"data: {json.dumps({'message': 'Waiting...', 'progress': progress})}\n\n"
                continue

            if isinstance(item, tuple) and item[0] == "__DONE__":
                _, success, errors, data = item
                payload = {
                    "done": True,
                    "success": success,
                    "progress": 100,
                    "message": "Complete!" if success else "; ".join(errors),
                    "type": "ok" if success else "error",
                }
                if data and data.get("dashboard_key"):
                    payload["dashboard_key"] = data["dashboard_key"]
                yield f"data: {json.dumps(payload)}\n\n"
                break
            else:
                progress = min(progress + step_increment, 95)
                payload = {"message": str(item), "progress": progress, "type": "ok"}
                yield f"data: {json.dumps(payload)}\n\n"

    resp = Response(generate(), mimetype="text/event-stream")
    resp.headers["Cache-Control"] = "no-cache"
    resp.headers["X-Accel-Buffering"] = "no"
    return resp
