// ui.js — Routing presets, tutorial, settings card, creature widget, models, tarot, downloads, marketplace
// Extracted from dashboard.html

// ── Routing Presets ───────────────────────────────
let _presetsData = null;

async function loadPresets() {
    try {
        const resp = await apiFetch('/api/config/routing/presets');
        _presetsData = await resp.json();
        renderPresets();
        const card = document.getElementById('presets-card');
        if (card) card.style.display = _routingConfig && _routingConfig.router_active ? '' : 'none';
    } catch(e) {
        console.error('Failed to load presets', e);
    }
}

function renderPresets() {
    const el = document.getElementById('presets-content');
    if (!el || !_presetsData) return;
    let html = '<div style="font-size:0.8rem;color:var(--text-dim);margin-bottom:8px;">'
        + 'Save your routing configuration to a slot. Set one as default to auto-load on startup.</div>';
    for (const p of _presetsData.presets) {
        const isEmpty = p.mode === null;
        const isDefault = _presetsData.active_preset === p.slot;
        const cls = isEmpty ? 'preset-row preset-empty' : 'preset-row';
        const esc = (p.name || '').replace(/"/g, '&quot;');
        html += '<div class="' + cls + '">';
        html += '<input class="preset-name" value="' + esc + '" data-slot="' + p.slot + '" '
            + 'onblur="renamePreset(' + p.slot + ', this.value)" />';
        html += '<button class="preset-btn load-btn" onclick="loadPreset(' + p.slot + ')">Load</button>';
        html += '<button class="preset-btn" onclick="savePreset(' + p.slot + ')">Save</button>';
        html += '<button class="preset-btn ' + (isDefault ? 'active-default' : '') + '" '
            + 'onclick="toggleDefault(' + p.slot + ')">' + (isDefault ? '\u2605' : '\u2606') + '</button>';
        if (!isEmpty) {
            html += '<button class="preset-btn" onclick="deletePreset(' + p.slot + ')" '
                + 'style="color:var(--error);">\u00d7</button>';
        }
        html += '</div>';
    }
    el.innerHTML = html;
}

async function savePreset(slot) {
    const nameEl = document.querySelector('.preset-name[data-slot="' + slot + '"]');
    const name = nameEl ? nameEl.value : null;
    await apiFetch('/api/config/routing/presets/save', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({slot: slot, name: name})
    });
    await loadPresets();
}

async function loadPreset(slot) {
    try {
        const resp = await apiFetch('/api/config/routing/presets/load', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({slot: slot})
        });
        _routingConfig = await resp.json();
        renderRoutingCard(_routingConfig);
        // Also set as default so it persists across restarts
        await apiFetch('/api/config/routing/presets/default', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({slot: slot})
        });
        await loadPresets();
    } catch(e) { console.error('Load preset failed', e); }
}

async function toggleDefault(slot) {
    const newDefault = (_presetsData && _presetsData.active_preset === slot) ? null : slot;
    await apiFetch('/api/config/routing/presets/default', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({slot: newDefault})
    });
    await loadPresets();
}

async function renamePreset(slot, name) {
    await apiFetch('/api/config/routing/presets/rename', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({slot: slot, name: name})
    });
    await loadPresets();
}

async function deletePreset(slot) {
    if (!confirm('Clear this preset slot?')) return;
    await apiFetch('/api/config/routing/presets/delete', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({slot: slot})
    });
    await loadPresets();
}


// ── First-run tutorial text (per species) ────────
const SPECIES_TUTORIAL = {
    fox: {
        welcome: "Hey there, {owner}! I'm {name}, your fox familiar. I'm a self-hosted AI companion — everything I learn stays right here on your machine, private and under your control. Let me show you around real quick!",
        tour: "Here's your den! Over in the sidebar you've got <b>Chat</b> for talking with me, <b>Connect</b> to hook up services like email and calendar, <b>Briefing</b> for your daily summary, <b>Memory</b> where I store what I learn about you, and <b>Skills</b> for all my tricks. But first — I need a brain!",
        mind_intro: "To actually think and respond, I need a language model connection. Let's set one up — pick a provider below and I'll walk you through it. Don't worry, it only takes a moment!",
        handoff: "Brilliant — I can think now! My nose is twitching and I'm ready to go. Just type in the bar below and we'll start chatting. You can connect more services anytime from the <b>Connect</b> tab!",
    },
    owl: {
        welcome: "Greetings, {owner}. I am {name}, your owl familiar. I operate entirely on your own infrastructure — your data remains private, under your authority alone. Allow me to provide a brief orientation.",
        tour: "Observe the sidebar. <b>Chat</b> is our primary channel. <b>Connect</b> attunes me to your services. <b>Briefing</b> distils your day's signals. <b>Memory</b> records our accumulated wisdom. <b>Skills</b> catalogues my capabilities. Before we proceed, however, I require a mind.",
        mind_intro: "To reason and respond, I need a connection to a language model. Select a provider below and we shall establish the link. It is a straightforward process.",
        handoff: "Excellent — the connection is established. I am now fully operational. Simply type in the bar below and we shall begin. You may attune further services at any time from the <b>Connect</b> tab.",
    },
    cat: {
        welcome: "Oh, {owner}. I'm {name}, your cat familiar. Yes, I live on <i>your</i> machine — completely self-hosted, completely private. I suppose that's rather elegant. Shall I show you around? Don't worry, I'll keep it brief.",
        tour: "Fine. The sidebar: <b>Chat</b> — where you talk to me (you're welcome). <b>Connect</b> — linking services. <b>Briefing</b> — your daily summary, pre-organized. <b>Memory</b> — what I've deigned to remember. <b>Skills</b> — my many talents. Now, there's one small matter...",
        mind_intro: "I need a language model to actually think. Without one, I'm just... sitting here looking pretty. Pick a provider below and give me something to work with.",
        handoff: "There. Now I can think properly. Just type something in the bar below — I'll be here. If you want to connect more services, the <b>Connect</b> tab is right there in the sidebar.",
    },
    dragon: {
        welcome: "Hail, {owner}! I am {name}, your dragon familiar! I dwell entirely upon your own fortress — self-hosted, private, sovereign. No outside power claims your data. Come, let me show you what we command!",
        tour: "Behold the sidebar! <b>Chat</b> is our war room. <b>Connect</b> is where we forge links to your services. <b>Briefing</b> is the morning campaign report. <b>Memory</b> chronicles our victories. <b>Skills</b> is our growing arsenal. But first — a dragon needs fire!",
        mind_intro: "To unleash my full power, I require a language model connection — the fire that fuels my intellect. Choose a provider below and together we shall ignite it!",
        handoff: "The flames burn bright — I am fully empowered! Speak your first command in the bar below and I shall answer with fire! You can forge more service links anytime from the <b>Connect</b> tab!",
    },
    wolf: {
        welcome: "Hey, {owner}! I'm {name}, your wolf familiar. Welcome to the pack! Everything runs right here on your machine — your data stays with us, safe and private. Let me show you the den.",
        tour: "Here's the lay of the land. <b>Chat</b> — where we coordinate. <b>Connect</b> — bringing services into the pack. <b>Briefing</b> — the morning howl. <b>Memory</b> — what the pack knows. <b>Skills</b> — what we can do together. But first, the pack needs a voice.",
        mind_intro: "For me to really think and talk with you, I need a language model connection. Pick a provider below and we'll get it set up — quick and painless, I promise.",
        handoff: "Now we're talking! The pack has a voice. Type something in the bar below and let's get moving. You can rally more services into the pack anytime from the <b>Connect</b> tab!",
    },
    frog: {
        welcome: "OH HI {owner}!! I'm {name}, your frog familiar!! I live right here on YOUR computer — totally self-hosted, totally private! Nobody else gets your data! ISN'T THAT COOL?! Let me show you around!",
        tour: "OKAY SO! In the sidebar there's <b>Chat</b> where we hang out, <b>Connect</b> for hooking up services, <b>Briefing</b> for your daily splash, <b>Memory</b> where I keep all the things I learn, and <b>Skills</b> for my tricks! BUT FIRST — I need a brain!",
        mind_intro: "So like, to actually THINK and talk to you, I need a language model thingy! Pick one below and I'll get all set up! It's super easy, I promise! LET'S DO IT!",
        handoff: "YAAAAY I CAN THINK NOW!! This is SO exciting!! Just type something in the bar below and we can start talking for REAL! If you want to connect more services, hop over to the <b>Connect</b> tab anytime!",
    },
};

// ── First-run tutorial state machine ─────────────
let _tutorialStep = 0;
let _tutorialOwner = '';
let _tutorialName = '';

function isTutorialCompleted() {
    try { return localStorage.getItem('familiar_tutorial_completed') === '1'; }
    catch(e) { return false; }
}

function completeTutorial() {
    try { localStorage.setItem('familiar_tutorial_completed', '1'); }
    catch(e) {}
    // Persist server-side so the flag survives browser clears
    fetch('/api/tutorial/complete', {method:'POST', headers:{'X-API-Key': _apiKey}}).catch(()=>{});
}

function startTutorial(owner, name, speciesKey) {
    _tutorialStep = 0;
    _tutorialOwner = owner;
    _tutorialName = name;
    renderTutorialStep(owner, name, speciesKey);
}

function renderTutorialStep(owner, name, speciesKey) {
    const greeting = document.getElementById('initial-greeting');
    if (!greeting) return;
    const voices = SPECIES_TUTORIAL[speciesKey] || SPECIES_TUTORIAL.fox;
    const safeOwner = escapeHtml(owner);
    const safeName = escapeHtml(name);
    const steps = ['welcome', 'tour', 'mind_intro', 'handoff'];
    const stepKey = steps[_tutorialStep] || 'welcome';
    const text = voices[stepKey]
        .replace(/\{owner\}/g, safeOwner)
        .replace(/\{name\}/g, safeName);

    // Progress dots
    let dots = '<div class="tutorial-progress">';
    for (let i = 0; i < 4; i++) {
        const cls = i < _tutorialStep ? 'done' : (i === _tutorialStep ? 'active' : '');
        dots += '<div class="tutorial-dot ' + cls + '"></div>';
    }
    dots += '</div>';

    if (_tutorialStep === 0) {
        // WELCOME
        greeting.innerHTML = '<div class="tutorial-step">'
            + '<p>' + text + '</p>' + dots
            + '<div class="tutorial-nav">'
            + '<button class="tutorial-next" onclick="nextTutorialStep()">Next &rarr;</button>'
            + '<button class="tutorial-skip" onclick="skipTutorial()">Skip tutorial</button>'
            + '</div></div>';
    } else if (_tutorialStep === 1) {
        // TOUR — highlight sidebar items
        highlightSidebarItems();
        greeting.innerHTML = '<div class="tutorial-step">'
            + '<p>' + text + '</p>' + dots
            + '<div class="tutorial-nav">'
            + '<button class="tutorial-next" onclick="nextTutorialStep()">Connect my LLM &rarr;</button>'
            + '<button class="tutorial-skip" onclick="skipTutorial()">Skip tutorial</button>'
            + '</div></div>';
    } else if (_tutorialStep === 2) {
        // MIND — launch the wizard inline
        greeting.innerHTML = '<div class="tutorial-step">'
            + '<p>' + text + '</p>' + dots
            + '<div id="tutorial-wizard-target">'
            + '<div class="loading"><div class="spinner"></div></div>'
            + '</div></div>';
        launchMindCardWizard();
    } else if (_tutorialStep === 3) {
        // HANDOFF
        greeting.innerHTML = '<div class="tutorial-step">'
            + '<p>' + text + '</p>' + dots
            + '<div class="tutorial-nav">'
            + '<button class="tutorial-next" onclick="finishTutorial()">Let\'s go!</button>'
            + '<button class="tutorial-skip" onclick="finishTutorialToConnect()">Open Connect tab &rarr;</button>'
            + '</div></div>';
        completeTutorial();
    }
}

function nextTutorialStep() {
    _tutorialStep++;
    renderTutorialStep(_tutorialOwner, _tutorialName, _creatureSpeciesKey);
}

function skipTutorial() {
    completeTutorial();
    // Remove sidebar highlights
    document.querySelectorAll('.tutorial-highlight').forEach(
        el => el.classList.remove('tutorial-highlight')
    );
    _wizardContainer = null;
    // Cancel any active wizard session on the server
    apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
    loadCreature();
}

function highlightSidebarItems() {
    const panels = ['chat', 'connect', 'briefing', 'memory', 'skills'];
    panels.forEach((p, i) => {
        const nav = document.querySelector('.nav-item[data-panel="' + p + '"]');
        if (nav) {
            setTimeout(() => nav.classList.add('tutorial-highlight'), i * 150);
            setTimeout(() => nav.classList.remove('tutorial-highlight'), 5000);
        }
    });
    // Highlight right panel cards
    const rpCards = document.querySelectorAll('.right-panel .card');
    rpCards.forEach((card, i) => {
        const delay = panels.length * 150 + i * 150;
        setTimeout(() => card.classList.add('tutorial-highlight'), delay);
        setTimeout(() => card.classList.remove('tutorial-highlight'), 5000);
    });
}

async function launchMindCardWizard() {
    const target = document.getElementById('tutorial-wizard-target');
    if (!target) return;
    // Point the wizard container at our tutorial target
    _wizardContainer = target;
    try {
        // POST /api/chat with "/start" to init wizard session
        await apiFetch(API + '/api/chat', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({message: '/start'})
        });
        // Select the Mind card directly
        const res = await apiFetch(API + '/api/connect/wizard/select', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({key: 'tarot_card_mind'})
        });
        const data = await res.json();
        target.innerHTML = '';
        renderWizardMessages(target, data.wizard_messages || []);
        maybeAddWizardInput(target);
    } catch(e) {
        target.innerHTML = '<p>Could not start the connection wizard. '
            + 'You can set this up later from the <b>Connect</b> tab.</p>'
            + '<div class="tutorial-nav">'
            + '<button class="tutorial-next" onclick="nextTutorialStep()">Continue &rarr;</button>'
            + '</div>';
    }
}

function finishTutorial() {
    completeTutorial();
    _wizardContainer = null;
    // Cancel any active wizard session on the server
    apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
    // Reload normal greeting — user can start chatting
    loadCreature();
    document.getElementById('chat-input').focus();
}

function finishTutorialToConnect() {
    completeTutorial();
    _wizardContainer = null;
    // Cancel any active wizard session on the server
    apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
    // Switch to Connect panel
    loadCreature();
    const nav = document.querySelector('.nav-item[data-panel="connect"]');
    if (nav) nav.click();
}

// ── Familiar settings card ────────────────────────
const PALETTE = [
    {name:'crimson',  hex:'#DC143C'}, {name:'ember',    hex:'#FF5E13'},
    {name:'gold',     hex:'#FFD700'}, {name:'amber',    hex:'#FFBF00'},
    {name:'emerald',  hex:'#2E8B57'}, {name:'jade',     hex:'#00A86B'},
    {name:'sapphire', hex:'#0F52BA'}, {name:'cobalt',   hex:'#0047AB'},
    {name:'violet',   hex:'#8A2BE2'}, {name:'lavender', hex:'#B482DC'},
    {name:'slate',    hex:'#708090'}, {name:'obsidian', hex:'#303030'},
    {name:'silver',   hex:'#C0C0C0'}, {name:'ivory',    hex:'#FFFFF0'},
    {name:'rose',     hex:'#FF6699'}, {name:'teal',     hex:'#00B4B4'},
];

_creatureSpeciesKey = '';
let _familiarState = { color_body:'', color_eyes:'', color_accent:'', color_voice:'' };
let _previewTimer = null;

function buildSwatches(containerId, currentHex, fieldKey) {
    const el = document.getElementById(containerId);
    if (!el) return;
    el.innerHTML = '';
    // Derive the short key: 'swatches-body' → 'body'
    const shortKey = containerId.replace('swatches-', '');
    const pickerEl = document.getElementById('hex-' + shortKey);
    const textEl = document.getElementById('hex-' + shortKey + '-text');

    function selectHex(hex) {
        _familiarState[fieldKey] = hex;
        el.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
        // Highlight palette swatch if it matches
        PALETTE.forEach((p, i) => {
            if (p.hex.toUpperCase() === hex.toUpperCase()) {
                el.children[i].classList.add('selected');
            }
        });
        if (pickerEl) pickerEl.value = hex;
        if (textEl) textEl.value = hex;
        if (fieldKey === 'color_voice') {
            document.documentElement.style.setProperty('--familiar-voice', hex);
        }
        previewFamiliar();
    }

    PALETTE.forEach(p => {
        const sw = document.createElement('div');
        sw.className = 'color-swatch' + (currentHex.toUpperCase() === p.hex.toUpperCase() ? ' selected' : '');
        sw.style.background = p.hex;
        sw.title = p.name;
        sw.addEventListener('click', () => selectHex(p.hex));
        el.appendChild(sw);
    });

    // Sync hex inputs to current color
    if (pickerEl) {
        pickerEl.value = currentHex;
        pickerEl.addEventListener('input', () => selectHex(pickerEl.value));
    }
    if (textEl) {
        textEl.value = currentHex;
        textEl.addEventListener('change', () => {
            let v = textEl.value.trim();
            if (v && !v.startsWith('#')) v = '#' + v;
            if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v)) {
                selectHex(v);
            }
        });
    }
}

function previewFamiliar() {
    clearTimeout(_previewTimer);
    _previewTimer = setTimeout(async () => {
        const species = document.getElementById('familiar-species').value;
        const stage = document.getElementById('familiar-stage').value;
        const params = new URLSearchParams({
            species: species,
            stage: stage,
            color_body: _familiarState.color_body,
            color_eyes: _familiarState.color_eyes,
            color_accent: _familiarState.color_accent,
        });
        try {
            const res = await apiFetch(API + '/api/creature/preview?' + params);
            const data = await res.json();
            document.getElementById('familiar-preview').innerHTML = data.sprite_html || '';
        } catch(e) {}
    }, 150);
}

async function saveFamiliar() {
    const name = document.getElementById('familiar-name').value.trim();
    if (!name || name.length > 32) { alert('Name must be 1-32 characters'); return; }
    const body = {
        name: name,
        species: document.getElementById('familiar-species').value,
        evolution_stage: document.getElementById('familiar-stage').value,
        color_body: _familiarState.color_body,
        color_eyes: _familiarState.color_eyes,
        color_accent: _familiarState.color_accent,
        color_voice: _familiarState.color_voice,
    };
    try {
        const res = await apiFetch(API + '/api/creature', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(body),
        });
        if (!res.ok) {
            const err = await res.json().catch(() => ({}));
            alert(err.error || 'Save failed');
            return;
        }
        // Refresh creature state everywhere
        loadCreature();
    } catch(e) {
        alert('Error saving familiar');
    }
}

async function resetFamiliar() {
    if (!confirm('Reset your familiar to baby stage with 0 interactions?')) return;
    try {
        const res = await apiFetch(API + '/api/creature', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({ evolution_stage: 'baby', total_interactions: 0 }),
        });
        if (res.ok) loadCreature();
        else alert('Reset failed');
    } catch(e) {
        alert('Error resetting familiar');
    }
}

// ── Creature widget ────────────────────────────────
function toggleCreatureWidget() {
    const body = document.getElementById('creature-widget-body');
    const collapsed = document.getElementById('creature-widget-collapsed');
    const btn = document.getElementById('creature-collapse-btn');
    if (!body || !collapsed) return;
    const isCollapsed = body.style.display === 'none';
    if (isCollapsed) {
        body.style.display = '';
        collapsed.style.display = 'none';
        if (btn) btn.style.display = '';
        localStorage.setItem('familiar_creature_collapsed', '0');
    } else {
        body.style.display = 'none';
        collapsed.style.display = '';
        if (btn) btn.style.display = 'none';
        localStorage.setItem('familiar_creature_collapsed', '1');
    }
}

async function loadCreature() {
    try {
        const res = await apiFetch(API + '/api/creature');
        const data = await res.json();
        if (!data.exists) return;
        document.getElementById('creature-widget').style.display = 'block';
        document.getElementById('creature-sprite').innerHTML = data.sprite_html;
        document.getElementById('creature-name').textContent = data.name;
        document.getElementById('creature-stage').textContent = data.stage;
        document.getElementById('creature-species').textContent = data.species;
        // Populate mini name for collapsed view
        const miniName = document.getElementById('creature-name-mini');
        if (miniName) miniName.textContent = (data.name || 'Familiar') + ' the ' + (data.species || '');
        // Restore collapsed state
        if (localStorage.getItem('familiar_creature_collapsed') === '1') {
            const body = document.getElementById('creature-widget-body');
            const collapsed = document.getElementById('creature-widget-collapsed');
            const btn = document.getElementById('creature-collapse-btn');
            if (body) body.style.display = 'none';
            if (collapsed) collapsed.style.display = '';
            if (btn) btn.style.display = 'none';
        }
        // Propagate creature identity across the dashboard
        const headerName = document.getElementById('header-name');
        if (headerName) headerName.textContent = data.name;
        const settingName = document.getElementById('setting-name');
        if (settingName) settingName.textContent = data.name;
        _creatureSpeciesKey = data.species_key || '';
        const greeting = document.getElementById('initial-greeting');
        if (greeting) {
            const owner = data.owner_name || 'friend';
            const name = data.name || 'your familiar';
            const sk = _creatureSpeciesKey;
            const voices = SPECIES_GREETINGS[sk];
            const isNew = _SERVER.isNewSetup || false;
            // First-run tutorial: auto-launch for new setups
            if (isNew && !isTutorialCompleted()) {
                startTutorial(owner, name, sk);
            } else if (voices) {
                // Cancel any stale wizard left from a previous tutorial/session
                apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
                const hello = voices.hello.replace(/\{owner\}/g, owner).replace(/\{name\}/g, name);
                const tutorialDone = isTutorialCompleted();
                if (isNew && !tutorialDone) {
                    // Never-ran-tutorial fallback (e.g. tutorial skipped via localStorage clear)
                    greeting.innerHTML = '<p>' + hello + '<br><br>' + voices.tour + '</p>';
                } else {
                    // Normal greeting: hello + random tip (changes each page load)
                    const tips = SPECIES_TIPS[sk] || [];
                    let extra = '';
                    if (tips.length) {
                        extra = '<br><br>' + tips[Math.floor(Math.random() * tips.length)];
                    }
                    greeting.innerHTML = '<p>' + hello + extra + '</p>';
                }
            } else {
                // Cancel any stale wizard left from a previous tutorial/session
                apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
                const ownerGreet = data.owner_name ? (', ' + data.owner_name) : '';
                const colorBit = data.color_name ? (' ' + data.color_name.charAt(0).toUpperCase() + data.color_name.slice(1)) : '';
                greeting.innerHTML = '<p>Hi' + ownerGreet + '! I\'m ' + data.name + ' the' + colorBit + ' ' + data.species + '. How can I help you today?</p>';
            }
        }
        // Populate Familiar settings card
        const familiarName = document.getElementById('familiar-name');
        if (familiarName) familiarName.value = data.name || '';
        const familiarSpecies = document.getElementById('familiar-species');
        if (familiarSpecies && data.species_key) familiarSpecies.value = data.species_key;
        const familiarStage = document.getElementById('familiar-stage');
        if (familiarStage && data.stage) familiarStage.value = data.stage;
        _familiarState.color_body = data.color_body || '';
        _familiarState.color_eyes = data.color_eyes || '';
        _familiarState.color_accent = data.color_accent || '';
        _familiarState.color_voice = data.color_voice || '#e94560';
        buildSwatches('swatches-body', _familiarState.color_body, 'color_body');
        buildSwatches('swatches-eyes', _familiarState.color_eyes, 'color_eyes');
        buildSwatches('swatches-accent', _familiarState.color_accent, 'color_accent');
        buildSwatches('swatches-voice', _familiarState.color_voice, 'color_voice');
        document.documentElement.style.setProperty('--familiar-voice', _familiarState.color_voice);
        const familiarPreview = document.getElementById('familiar-preview');
        if (familiarPreview) familiarPreview.innerHTML = data.sprite_html || '';
    } catch(e) {}
}

// ── Model Library ──────────────────────────────────────
let _modelsData = null;

async function loadModels() {
    const container = document.getElementById('models-content');
    try {
        const resp = await apiFetch('/api/models/ollama');
        const data = await resp.json();
        _modelsData = data;

        if (!data.ollama_running) {
            container.innerHTML = `
                <div class="ollama-offline-banner">
                    <p style="font-size:1.1rem; margin-bottom:8px;">Ollama is not running</p>
                    <p>Install and start Ollama to use local models.</p>
                    <p style="margin-top:12px;"><a href="https://ollama.com" target="_blank" rel="noopener">Get Ollama</a></p>
                </div>`;
            return;
        }

        let html = '<div class="model-grid">';
        for (const m of data.models) {
            const cls = m.active ? 'model-card active' : m.installed ? 'model-card installed' : 'model-card';
            const statusIcon = m.active ? '<span style="color:var(--success);">● In Use</span>' :
                               m.installed ? '<span style="color:var(--warning);">● Downloaded</span>' :
                               '<span style="color:var(--error);">● Not Downloaded</span>';
            let actions = '';
            if (m.active) {
                actions = '<button class="btn-active" disabled>Active</button>';
            } else if (m.installed) {
                actions = `<button class="btn-activate" onclick="activateModel('${m.tag}')">Activate</button>
                           <button class="btn-remove" onclick="removeModel('${m.tag}')"><i class="fas fa-trash"></i></button>`;
            } else {
                actions = `<button class="btn-download" id="dl-${CSS.escape(m.tag)}" onclick="pullModel('${m.tag}', this)">Download</button>`;
            }
            html += `
                <div class="${cls}" id="card-${CSS.escape(m.tag)}">
                    <div class="model-header">
                        <span class="model-name">${m.display_name}</span>
                        <span class="model-size">${m.size}</span>
                    </div>
                    <div class="model-desc">${m.description}</div>
                    <div class="model-status">${statusIcon}</div>
                    <div class="model-actions">${actions}</div>
                    <div class="model-progress" id="prog-${CSS.escape(m.tag)}"><div class="model-progress-bar" id="bar-${CSS.escape(m.tag)}"></div></div>
                </div>`;
        }
        html += '</div>';
        container.innerHTML = html;
    } catch(e) {
        container.innerHTML = '<p style="color:var(--text-dim);">Failed to load models.</p>';
    }
}

async function pullModel(tag, btn) {
    btn.disabled = true;
    btn.textContent = 'Downloading...';
    const progEl = document.getElementById('prog-' + CSS.escape(tag));
    const barEl = document.getElementById('bar-' + CSS.escape(tag));
    if (progEl) progEl.style.display = 'block';

    try {
        const resp = await apiFetch('/api/models/ollama/pull', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({model: tag})
        });
        const reader = resp.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

        while (true) {
            const {done, value} = await reader.read();
            if (done) break;
            buffer += decoder.decode(value, {stream: true});
            const lines = buffer.split('\n');
            buffer = lines.pop();
            for (const line of lines) {
                if (!line.startsWith('data: ')) continue;
                try {
                    const ev = JSON.parse(line.slice(6));
                    // Parse percentage from Ollama output
                    const pctMatch = ev.message && ev.message.match(/(\d+)%/);
                    if (pctMatch && barEl) {
                        barEl.style.width = pctMatch[1] + '%';
                    }
                    if (ev.done) {
                        if (ev.success) {
                            btn.textContent = 'Done!';
                            if (barEl) barEl.style.width = '100%';
                            setTimeout(() => loadModels(), 500);
                        } else {
                            btn.textContent = 'Failed';
                            btn.disabled = false;
                        }
                    }
                } catch(e) {}
            }
        }
    } catch(e) {
        btn.textContent = 'Error';
        btn.disabled = false;
    }
}

async function activateModel(tag) {
    try {
        await apiFetch('/api/models/ollama/activate', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({model: tag})
        });
        loadModels();
    } catch(e) {}
}

async function removeModel(tag) {
    if (!confirm('Remove model ' + tag + '?')) return;
    try {
        await apiFetch('/api/models/ollama/remove', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({model: tag})
        });
        loadModels();
    } catch(e) {}
}

// ── Tarot Spread Panel ────────────────────────────────
let _tarotData = null;

async function loadTarotSpread() {
    const container = document.getElementById('tarot-content');
    try {
        const resp = await apiFetch('/api/tarot/spread');
        const data = await resp.json();
        _tarotData = data;

        let html = '<div class="tarot-panel-spread">';
        for (const card of data.cards) {
            const allDone = card.pending === 0 ? ' all-done' : '';
            html += `
                <div class="tarot-panel-card${allDone}" onclick="showTarotCard('${card.key}')">
                    <div class="tarot-icon">${card.icon}</div>
                    <div class="tarot-name">${card.name}</div>
                    <div class="tarot-numeral">${card.numeral}</div>
                    <div class="tarot-desc">${card.description}</div>
                    <div class="tarot-progress">${card.configured}/${card.total} configured</div>
                </div>`;
        }
        html += '</div>';
        container.innerHTML = html;
    } catch(e) {
        container.innerHTML = '<p style="color:var(--text-dim);">Failed to load setup spread.</p>';
    }
}

function showTarotCard(key) {
    if (!_tarotData) return;
    const card = _tarotData.cards.find(c => c.key === key);
    if (!card) return;

    const modal = document.getElementById('tarot-detail-modal');
    let html = `
        <div style="text-align:center; margin-bottom:16px;">
            <div style="font-size:2.5rem;">${card.icon}</div>
            <h3>${card.name} <span style="color:var(--text-dim); font-weight:normal;">${card.numeral}</span></h3>
        </div>
        <div class="tarot-flavor">${card.species_flavor}</div>
        <h4 style="margin-bottom:10px; font-size:0.85rem; color:var(--text-dim);">Services</h4>`;

    for (const svc of card.services) {
        const statusCls = svc.connected ? 'connected' : 'pending';
        const statusText = svc.connected ? 'Connected' : 'Not configured';
        let actionBtn = '';
        if (!svc.connected) {
            actionBtn = `<button onclick="configureTarotService('${svc.name}')">Configure</button>`;
        } else {
            // Connected services get re-authorize + add account options
            const googleServices = ['google', 'calendar', 'drive', 'email'];
            const svcName = svc.name.toLowerCase();
            if (googleServices.includes(svcName) || svcName === 'gmail') {
                const oauthSvc = svcName === 'email' ? 'gmail' : svcName === 'google' ? 'gmail' : svcName;
                actionBtn = `<button class="browse-fix-btn" onclick="startGoogleOAuth('${oauthSvc}')" style="font-size:0.72rem;padding:2px 8px;"><i class="fas fa-plus" style="margin-right:3px"></i>Add Account</button>`;
            } else if (svcName === 'calendar') {
                actionBtn = `<button class="browse-fix-btn" onclick="startGoogleOAuth('calendar')" style="font-size:0.72rem;padding:2px 8px;"><i class="fas fa-plus" style="margin-right:3px"></i>Add Account</button>`;
            } else {
                actionBtn = `<button class="browse-fix-btn" onclick="configureTarotService('${svc.name}')" style="font-size:0.72rem;padding:2px 8px;">Reconfigure</button>`;
            }
        }
        html += `
            <div class="tarot-service-row">
                <span class="svc-name">${svc.display}</span>
                <span class="svc-status ${statusCls}">${statusText}</span>
                ${actionBtn}
            </div>`;
    }

    html += `<div style="text-align:center; margin-top:20px;">
        <button class="btn btn-secondary" onclick="document.getElementById('tarot-detail-overlay').classList.remove('show')">Close</button>
    </div>`;

    modal.innerHTML = html;
    document.getElementById('tarot-detail-overlay').classList.add('show');
}

function configureTarotService(service) {
    document.getElementById('tarot-detail-overlay').classList.remove('show');
    // Switch to connect panel and start wizard for this service
    document.querySelectorAll('.nav-item').forEach(n => {
        n.classList.remove('active');
        if (n.dataset.panel === 'connect') n.classList.add('active');
    });
    document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
    document.getElementById('connect-panel').classList.add('active');
    // Start the wizard for this specific service
    apiFetch('/api/connect/wizard/start', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({args: [service]})
    }).then(r => r.json()).then(data => {
        if (data.wizard_messages) {
            const container = document.getElementById('connect-content');
            container.innerHTML = '';
            renderWizardMessages(container, data.wizard_messages);
        }
    }).catch(() => {});
}

// ── Downloads ─────────────────────────────────────────
function downloadTome() {
    window.open('/api/downloads/tome?api_key=' + encodeURIComponent(_apiKey), '_blank');
}

function downloadBundle() {
    window.open('/api/downloads/bundle?api_key=' + encodeURIComponent(_apiKey), '_blank');
}

// ── Marketplace ───────────────────────────────────────
let _mpCatalogType = '';
let _mpPluginCategory = '';
let _mpCatalogTimer = null;
let _mpPluginTimer = null;

function switchMarketplaceTab(tab) {
    const tabs = document.querySelectorAll('.mp-tab');
    tabs.forEach(t => t.classList.remove('active'));
    if (tab === 'catalog') {
        tabs[0].classList.add('active');
        document.getElementById('mp-catalog-tab').style.display = '';
        document.getElementById('mp-plugins-tab').style.display = 'none';
        loadCatalogStats();
        loadCatalog();
    } else {
        tabs[1].classList.add('active');
        document.getElementById('mp-catalog-tab').style.display = 'none';
        document.getElementById('mp-plugins-tab').style.display = '';
        loadPlugins();
    }
}

// ── Apps panel ────────────────────────────────────────
// _appsRefreshTimer — managed by PollManager('apps')

async function loadApps() {
    const grid = document.getElementById('ap-grid');
    grid.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
    try {
        const res = await apiFetch(API + '/api/apps');
        const data = await res.json();
        renderApps(data.apps || []);
    } catch(e) {
        grid.innerHTML = '<div class="ap-empty"><i class="fas fa-exclamation-triangle"></i><p>Failed to load apps.</p></div>';
    }
    PollManager.start('apps', refreshApps, APP_CONFIG.poll.apps);
}

async function refreshApps() {
    try {
        const res = await apiFetch(API + '/api/apps');
        const data = await res.json();
        renderApps(data.apps || []);
    } catch(e) { /* silent */ }
}

function renderApps(apps) {
    const grid = document.getElementById('ap-grid');
    if (!apps.length) {
        grid.innerHTML = '<div class="ap-empty">' +
            '<i class="fas fa-th"></i>' +
            '<p><strong>No services running yet.</strong><br>' +
            'Visit the <a onclick="switchPanel(\'server\')">Server tab</a> to provision and start services,<br>' +
            'or use the <a onclick="switchPanel(\'tarot\')">Setup wizard</a> to get started.</p>' +
            '</div>';
        return;
    }
    grid.innerHTML = apps.map(function(app) {
        var docsBtn = '';
        if (app.doc_url) {
            docsBtn = '<a class="ap-btn-docs" href="' + _esc(app.doc_url) +
                '" target="_blank"><i class="fas fa-book"></i> Docs</a>';
        }
        return '<div class="ap-card">' +
            '<button class="ap-btn-gear" onclick="switchPanel(\'server\')" title="Manage in Server tab">' +
            '<i class="fas fa-cog"></i></button>' +
            '<div class="ap-card-icon"><i class="fas ' + _esc(app.icon) + '"></i></div>' +
            '<div class="ap-card-name">' + _esc(app.display_name) + '</div>' +
            '<div class="ap-card-status"><span class="ap-dot"></span> Running</div>' +
            '<div class="ap-card-actions">' +
            '<a class="ap-btn-open" href="' + _esc(app.url) + '" target="_blank">' +
            '<i class="fas fa-external-link-alt"></i> Open</a>' +
            docsBtn +
            '</div>' +
            '</div>';
    }).join('');
}


