<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   The one definition of what an AI profile is. Every consumer — the endpoint, the worker, the
//   shared chat include and every page that renders it — reads it from here instead of
//   restating it.
//
// WHY THIS EXISTS
//   A profile used to be defined in five places: history depth in api/ai.php, capabilities in
//   include/ai.php, label/hint/depth again in the chat's JavaScript, a prompt branch in the
//   worker, and a label map in pages/scheduler.php. They had already drifted — the JavaScript
//   knew three profiles where PHP knew four, so the shared chat could not offer troubleshoot at
//   all and the Scheduler dock hand-rolled its own labels to compensate. The include carried a
//   comment telling the next person not to let the two tables diverge, which is a comment doing
//   a data structure's job.
//
// WHAT LIVES HERE, AND WHAT DELIBERATELY DOES NOT
//   Here: anything more than one file needs to agree on — the set of profiles, their labels and
//   hints, history depth, capabilities, and whether a profile is offered as a button.
//
//   Not here: the system prompts. They are long, delicate, and have exactly one consumer, so
//   moving them would be churn against the most sensitive text in the subsystem for no reduction
//   in duplication. Tools/ai_chat_worker.php still owns them; it just keys off ids validated
//   here rather than an if-chain that invents its own vocabulary.
//
// CAPABILITIES ARE PER PROFILE, NOT PER CAPABILITY
//   The old table was inverted — capability => [profiles] — which reads well when adding a
//   capability and badly when answering the question actually asked at runtime, which is always
//   "what can this profile do". Same content, turned the right way round.
//
//   A profile is a contract plus a set of inputs, and the inputs are the half that has to be
//   enforced rather than requested. The caps list is that half.
//
//   It exists because the alternative already failed. The same permissions used to live as a
//   dozen `$profile === 'varaverk' || $profile === 'troubleshoot'` conditions spread across the
//   worker, and answering "may chat ever be shown a log?" meant reading all of them. It could —
//   a gate added for run-outcome questions granted it by omission, and the chat profile, whose
//   entire value is that it has NOT been shown this installation, was one phrasing away from
//   being handed a health sweep and 120 lines of log. Nothing about that was visible at the
//   point of the mistake. Here it would have been one missing word on one line.
//
//   A capability is permission, not need. varaverk holds 'health' but only attaches it when the
//   question looks diagnostic; troubleshoot attaches it always. The gates decide whether an
//   input is warranted, this decides whether it is allowed, and a gate can never widen the grant.
//
//   chat holding an empty capability list is a guarantee, not an oversight. Anything added to it
//   stops being general chat and becomes an assistant that sometimes lies about this
//   installation.
//
// EXPORTS
//   vv_ai_profiles()            the whole table
//   vv_ai_profile($id)          one entry, or null
//   vv_ai_profile_ok($id)       is this a real profile
//   vv_ai_profile_turns($id)    history depth in turns
//   vv_ai_profile_can($id,$cap) capability check
//   vv_ai_profile_caps($id)     every capability a profile holds
//   vv_ai_profile_label($id)    display name, falling back to the id
//   vv_ai_profiles_ui()         those offered as buttons, in order
//   vv_ai_profiles_max_turns()  deepest window of any profile
//   vv_ai_profiles_client()     the subset the browser needs, for json_encode
//   vv_ai_profiles_script()     publishes that subset as window.VvAiProfiles, once per page
// ═══════════════════════════════════════════════════════════════════════════════════════════════

// turns  — conversation depth. Varaverk Assistant spends ~2500 of its 16384 on retrieved
//          passages so it cannot afford much; troubleshoot carries a whole log tail and can
//          afford least of all; the two that retrieve nothing can hold a real conversation.
// caps   — see VV_AI_CAP_MEANING below.
// ui     — appears in the profile bar. troubleshoot is false because it is entered by opening a
//          log on the Scheduler tab, not by choosing it: it is a context you are in, and a
//          button offering it from the dashboard would produce a troubleshooting contract with
//          nothing to troubleshoot.
const VV_AI_PROFILES_DEF = [
    'varaverk' => [
        'label' => 'Varaverk Assistant',
        'short' => 'Assistant',
        'hint'  => 'Answers only from Varaverk\'s own docs, with sources. Says so when they don\'t cover it.',
        'turns' => 3,
        'ui'    => true,
        'caps'  => ['retrieve', 'kind_filter', 'health', 'run_evidence', 'incidents', 'conf_lookup'],
    ],
    'chat' => [
        'label' => 'General Chat',
        'short' => 'Chat',
        'hint'  => 'Ordinary conversation. Hands anything about this install to the assistant on its own.',
        'turns' => 8,
        'ui'    => true,
        // The only capability general chat holds, and it holds it *because* chat cannot write.
        // Search is a read that leaves the house; every other capability in this table either
        // reads this installation or changes it, and neither belongs on the profile whose
        // contract is that it has no reach into the machine at all. The Varaverk assistant is
        // deliberately not given it: its contract is that answers come from this installation's
        // own documents, and a web result there is an answer that looks sourced and is not.
        'caps'  => ['web_search'],
    ],
    'code' => [
        'label' => 'Code Sketcher',
        'short' => 'Code',
        'hint'  => 'Drafts short scripts for Custom Scripts. First drafts — test before trusting.',
        'turns' => 4,
        'ui'    => true,
        'caps'  => ['code_scan'],
    ],
    // Offered as a button as well as entered by context. It was context-only on the reasoning
    // that choosing it from a dashboard produces a troubleshooting contract with nothing to
    // troubleshoot — true of the inputs, but not of the profile: name a script in the question
    // and run_evidence resolves the run record and log for it without any scope at all. The
    // prompt now states which of those it actually received rather than asserting a log, so
    // picking it with nothing open degrades to an honest "I cannot see one" instead of an
    // invented reading of a log that was never attached.
    'troubleshoot' => [
        'label' => 'Troubleshoot',
        'short' => 'Troubleshoot',
        'hint'  => 'Reasons from evidence — the log you have open, or one you name. Says so when there is none.',
        'turns' => 2,
        'ui'    => true,
        // system_state is read-only and shared with repair. Both need to know a container is down
        // or a pool is full to explain anything about this machine rather than about Unraid in
        // general; neither gets a way to act on it, and only repair can change a setting.
        'caps'  => ['retrieve', 'health', 'system_state', 'run_evidence', 'scoped_log',
                    'incidents', 'conf_lookup', 'file_bugs'],
    ],
    // The only profile that may change a setting, and the only one not offered as a button.
    //
    // It is entered by opening a finding, the way troubleshoot was originally entered by opening
    // a log — a repair conversation with nothing to repair is a contract with no subject. That is
    // not the only reason though: capabilities are granted per profile, so making this selectable
    // would put conf_write one click away from any question at all. The narrow grant is the point.
    //
    // Reads two records of what has happened before. The phrasebook holds what the operator calls
    // things and, more usefully, what they have had to correct; closed findings hold what actually
    // fixed a fault last time. Between them the profile starts a conversation already knowing the
    // operator's vocabulary and this installation's history, instead of relearning both each time.
    'repair' => [
        'label' => 'Repair',
        'short' => 'Repair',
        'hint'  => 'Works through a finding with you, and can apply a fix you approve.',
        'turns' => 3,
        'ui'    => false,
        'caps'  => ['retrieve', 'health', 'system_state', 'run_evidence', 'scoped_log', 'incidents',
                    'conf_lookup', 'conf_write', 'probe', 'file_findings', 'phrasebook', 'past_fixes'],
    ],
];

// Documentation only — nothing branches on it. Kept beside the table because a capability name
// with no stated meaning is how one ends up granted to a profile that should not have it.
const VV_AI_CAP_MEANING = [
    'retrieve'     => 'retrieval passages from the documentation index',
    'kind_filter'  => 'the retrieval kind filter the page exposes',
    'health'       => 'live health sweep measured at question time — the AI subsystem only',
    'system_state' => 'read-only view of the machine: hardware, containers, pools, array, UPS',
    'run_evidence' => 'run record and log tail for a script named in the question',
    'scoped_log'   => 'log tail for whatever the operator currently has open',
    'incidents'    => 'operator-written history of what previously went wrong with this thing',
    'conf_lookup'  => 'deterministic "where does this conf key actually live" lookup',
    'file_bugs'    => 'may file a bug report against Varaverk itself',
    'code_scan'    => 'destructive-operation scan of generated shell',
    'conf_write'   => 'may change a setting — through the guarded write path, never directly',
    'probe'        => 'may test a candidate address before anything is written to conf',
    'file_findings'=> 'may record, acknowledge and close findings about this installation',
    'phrasebook'   => 'what the operator calls things, and what they have corrected before',
    'past_fixes'   => 'findings already closed — what fixed this last time',
    'web_search'   => 'may search the web — the one capability that sends text off this machine',
];

function vv_ai_profiles(): array { return VV_AI_PROFILES_DEF; }

function vv_ai_profile(string $id): ?array {
    return VV_AI_PROFILES_DEF[$id] ?? null;
}

function vv_ai_profile_ok(string $id): bool {
    return isset(VV_AI_PROFILES_DEF[$id]);
}

// Falls back rather than throwing. An unknown id reaching here is a caller that failed to
// validate, and the shallowest window is the safe way to be wrong: it spends the least context.
function vv_ai_profile_turns(string $id): int {
    return (int)(VV_AI_PROFILES_DEF[$id]['turns'] ?? 3);
}

function vv_ai_profile_caps(string $id): array {
    return VV_AI_PROFILES_DEF[$id]['caps'] ?? [];
}

function vv_ai_profile_can(string $id, string $cap): bool {
    return in_array($cap, VV_AI_PROFILES_DEF[$id]['caps'] ?? [], true);
}

function vv_ai_profile_label(string $id): string {
    return VV_AI_PROFILES_DEF[$id]['label'] ?? $id;
}

// Insertion order is the button order, and varaverk is first because the strict profile is the
// one to land on: a misuse there costs "the docs don't cover that" rather than an invented claim
// about the system.
function vv_ai_profiles_ui(): array {
    return array_filter(VV_AI_PROFILES_DEF, fn($p) => !empty($p['ui']));
}

// The cap api/ai.php applies when storing a conversation, which has to hold the deepest window
// any profile could later ask for — a chat saved under one profile can be reopened under another.
function vv_ai_profiles_max_turns(): int {
    return max(array_column(VV_AI_PROFILES_DEF, 'turns'));
}

// What the browser needs, and nothing more. The prompts are not here to be sent and the server
// re-derives depth and capability on every request regardless — this is for drawing buttons and
// deciding whether to show the kind filter, not for the client to make policy with.
function vv_ai_profiles_client(): array {
    $out = [];
    foreach (VV_AI_PROFILES_DEF as $id => $p) {
        $out[$id] = [
            'label' => $p['label'],
            'short' => $p['short'],
            'hint'  => $p['hint'],
            'turns' => $p['turns'],
            'ui'    => (bool)$p['ui'],
            'kind'  => in_array('kind_filter', $p['caps'], true),
            // Whether to offer the search box, on the same terms as the kind filter: derived from
            // the capability rather than named per profile, so granting or removing it in the
            // table above is the only edit either side needs.
            'web'   => in_array('web_search', $p['caps'], true),
        ];
    }
    return $out;
}

// Publishes the registry to the browser as window.VvAiProfiles, once per page however many
// surfaces ask for it. Emitted as its own tag so any script block that consumes it stays pure
// JavaScript and remains syntax-checkable outside PHP.
//
// The shared chat include calls this, as does any page emitting the registry ahead of it. Before
// it existed the Scheduler had its own literal `{ code: 'Code', troubleshoot: 'Troubleshoot' }`
// map, which is how a fourth copy of the profile list came to exist in the first place.
function vv_ai_profiles_script(): void {
    static $done = false;
    if ($done) return;
    $done = true;
    echo '<script>window.VvAiProfiles = '
       . json_encode(vv_ai_profiles_client(), JSON_UNESCAPED_SLASHES) . ";</script>\n";
}
