/**
 * Follow-the-loaded-model resolution.
 *
 * The model RHA runs is decided by WHAT THE USER LOADED in their local server
 * (LM Studio's load button is the control surface) — never by list order
 * (silent-swap hazard) and never by a hardcoded id (control taken from the
 * user). An explicit vault override remains available for a hard pin.
 *
 * Resolution is per-job and recorded in usage/debug records, so a change of
 * loaded model is always LOUD, never silent.
 *
 * If NOTHING is loaded, resolution fails honestly — we do not JIT-load the
 * first listed model (that is how a 22 GB model gets loaded by accident).
 */

export interface EndpointModel {
  id: string;
  loaded: boolean;
  /** 'llm' | 'vlm' | 'embedding' | unknown */
  type?: string;
  /**
   * SET 22 — host generation status when the endpoint exposes it
   * (idle/ready vs busy/generating). Absent → treat loaded as idle.
   */
  status?: string;
}

/** LM Studio native v1: { models: [{ key, type, loaded_instances: [...] }] } */
export function parseNativeV1(json: unknown): EndpointModel[] | null {
  const j = json as {
    models?: {
      key?: string;
      type?: string;
      loaded_instances?: { status?: string; state?: string; busy?: boolean }[];
      status?: string;
    }[];
  };
  if (!Array.isArray(j?.models)) return null;
  return j.models
    .filter(m => typeof m?.key === 'string')
    .map(m => {
      const instances = Array.isArray(m.loaded_instances) ? m.loaded_instances : [];
      const loaded = instances.length > 0;
      const instStatus = instances
        .map(i => i?.status || i?.state || (i?.busy ? 'busy' : undefined))
        .find(Boolean);
      return {
        id: m.key as string,
        loaded,
        type: m.type,
        status: instStatus || m.status,
      };
    });
}

/** LM Studio native v0: { data: [{ id, type, state: 'loaded' | 'not-loaded' }] } */
export function parseNativeV0(json: unknown): EndpointModel[] | null {
  const j = json as {
    data?: { id?: string; type?: string; state?: string; status?: string }[];
  };
  if (!Array.isArray(j?.data)) return null;
  const withState = j.data.filter(m => typeof m?.id === 'string' && typeof m?.state === 'string');
  if (!withState.length) return null; // OpenAI-shaped, no load state — not this parser's job
  return withState.map(m => ({
    id: m.id as string,
    loaded: m.state === 'loaded',
    type: m.type,
    status: m.status || m.state,
  }));
}

/** Plain OpenAI-compat: { data: [{ id }] } — order only, no load state. */
export function parseOpenAi(json: unknown): EndpointModel[] | null {
  const j = json as { data?: { id?: string }[] };
  if (!Array.isArray(j?.data)) return null;
  return j.data.filter(m => typeof m?.id === 'string').map(m => ({ id: m.id as string, loaded: false }));
}

const isEmbedding = (m: EndpointModel) =>
  m.type === 'embedding' || /embed/i.test(m.id);

/** Pick the model the user loaded; null when nothing is loaded (fail honestly). */
export function pickLoaded(models: EndpointModel[]): string | null {
  const loaded = models.filter(m => m.loaded && !isEmbedding(m));
  return loaded[0]?.id ?? null;
}

async function fetchJson(url: string, apiKey?: string): Promise<unknown | null> {
  try {
    const headers: Record<string, string> = {};
    if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
    const r = await fetch(url, { headers, signal: AbortSignal.timeout(4000) });
    if (!r.ok) return null;
    return await r.json();
  } catch {
    return null;
  }
}

export interface ActiveModel {
  id: string;
  /** 'loaded' = the user's load state decided; 'first-listed' = endpoint has no load-state API. */
  source: 'loaded' | 'first-listed';
}

/**
 * Resolve the active model for an OpenAI-compat endpoint.
 *  - LM Studio (native API present): the LOADED model, else null — nothing
 *    loaded means the user has not chosen; we refuse to guess.
 *  - Plain OpenAI-compat (no native API): first listed (cloud endpoints serve
 *    everything; there is no "loaded" concept to follow).
 */
export async function resolveActiveModel(baseURL: string, apiKey?: string): Promise<ActiveModel | null> {
  const origin = baseURL.replace(/\/v1\/?$/, '');
  for (const path of ['/api/v1/models', '/api/v0/models']) {
    const json = await fetchJson(`${origin}${path}`, apiKey);
    if (json == null) continue;
    const models = parseNativeV1(json) ?? parseNativeV0(json);
    if (models) {
      const id = pickLoaded(models);
      return id ? { id, source: 'loaded' } : null;
    }
  }
  const json = await fetchJson(`${baseURL.replace(/\/$/, '')}/models`, apiKey);
  const models = json != null ? parseOpenAi(json) : null;
  const first = models?.filter(m => !isEmbedding(m))[0]?.id;
  return first ? { id: first, source: 'first-listed' } : null;
}

/** SET 22 — host generation posture for desync detection. */
export type HostStatus = 'busy' | 'idle' | 'unreachable';

const BUSY_RE = /^(busy|generating|gen|predicting|running|in[_-]?progress)$/i;
const IDLE_RE = /^(idle|ready|loaded|not-loaded|available)$/i;

/** Infer idle vs busy from parsed endpoint models (unit-tested). */
export function inferHostStatus(models: EndpointModel[]): HostStatus {
  const relevant = models.filter(m => !isEmbedding(m));
  if (!relevant.length) return 'idle';
  for (const m of relevant) {
    const s = (m.status || '').trim();
    if (s && BUSY_RE.test(s)) return 'busy';
  }
  // Any loaded model without a busy flag ⇒ host is idle/ready (Andy’s live probe).
  void IDLE_RE; // documented idle tokens; absence of busy is enough
  return 'idle';
}

/**
 * Probe the LLM host for idle-vs-generating. Unreachable ⇒ 'unreachable'
 * (transport recovery may still land the in-flight call — keep waiting).
 */
export async function probeHostStatus(baseURL: string, apiKey?: string): Promise<HostStatus> {
  const origin = baseURL.replace(/\/v1\/?$/, '');
  let sawAny = false;
  for (const path of ['/api/v1/models', '/api/v0/models']) {
    const json = await fetchJson(`${origin}${path}`, apiKey);
    if (json == null) continue;
    sawAny = true;
    const models = parseNativeV1(json) ?? parseNativeV0(json);
    if (models) return inferHostStatus(models);
  }
  const json = await fetchJson(`${baseURL.replace(/\/$/, '')}/models`, apiKey);
  if (json != null) {
    sawAny = true;
    const models = parseOpenAi(json);
    if (models) return inferHostStatus(models);
  }
  return sawAny ? 'idle' : 'unreachable';
}
