/**
 * Brain-API-Client fuer die App.
 *
 * Die App hat keinen direkten HTTP-Zugriff aufs Brain (nur via RVS). Wir
 * tunneln alle Memory-Operationen ueber den generischen brain_request /
 * brain_response RVS-Channel den die Bridge implementiert.
 *
 * Pattern: pro Call eine eindeutige requestId, Listener wartet auf passende
 * brain_response, Promise loest auf / wird abgelehnt bei status>=400.
 */

import rvs from './rvs';

type AnyJson = any;

interface PendingRequest {
  resolve: (data: AnyJson) => void;
  reject: (err: Error) => void;
  timer: ReturnType<typeof setTimeout>;
  expectBinary?: boolean;
}

const pending = new Map<string, PendingRequest>();
let installed = false;

function _ensureListener() {
  if (installed) return;
  installed = true;
  rvs.onMessage((msg: any) => {
    if (!msg || msg.type !== 'brain_response') return;
    const p = msg.payload || {};
    const reqId: string = p.requestId || '';
    const handler = pending.get(reqId);
    if (!handler) return;
    pending.delete(reqId);
    clearTimeout(handler.timer);
    const status: number = Number(p.status || 0);
    if (status >= 200 && status < 300) {
      if (handler.expectBinary) {
        handler.resolve({ base64: p.base64 || '', contentType: p.contentType || '' });
      } else {
        handler.resolve(p.json !== undefined ? p.json : (p.text !== undefined ? p.text : null));
      }
    } else {
      const detail = (p.json && p.json.detail) || p.text || `HTTP ${status}`;
      handler.reject(new Error(`Brain ${status}: ${detail}`));
    }
  });
}

let _nextId = 0;
function _newRequestId(): string {
  _nextId += 1;
  return `brain_${Date.now().toString(36)}_${_nextId}`;
}

/** Mini-Query-String-Builder ohne URLSearchParams (Hermes-Polyfill kennt
 *  kein URLSearchParams.set, crasht). Akzeptiert object mit string/number/
 *  bool-Values; undefined/null/leere Strings werden ausgelassen. */
function _qs(params: Record<string, unknown>): string {
  const parts: string[] = [];
  for (const [k, v] of Object.entries(params)) {
    if (v === undefined || v === null || v === '') continue;
    parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
  }
  return parts.length ? `?${parts.join('&')}` : '';
}

interface SendOpts {
  method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
  body?: AnyJson;
  bodyBase64?: string;
  contentType?: string;
  expectBinary?: boolean;
  timeoutMs?: number;
}

function _send(path: string, opts: SendOpts = {}): Promise<AnyJson> {
  _ensureListener();
  // Fast-Fail wenn RVS nicht verbunden — sonst tickt der Timeout 30s und
  // der TriggerBrowser / Dateimanager zeigt ne ewig drehende Spinner.
  // Stefan-Bug 06/2026: "Connection refused, App haengt 30 Sekunden".
  const rvsState = rvs.getState();
  if (rvsState !== 'connected') {
    return Promise.reject(new Error(
      `Keine Verbindung zum Brain (RVS: ${rvsState}). Warte auf Reconnect...`,
    ));
  }
  return new Promise((resolve, reject) => {
    const requestId = _newRequestId();
    const timer = setTimeout(() => {
      if (pending.delete(requestId)) {
        reject(new Error(`Brain-Timeout fuer ${path}`));
      }
    }, opts.timeoutMs || 30000);
    pending.set(requestId, { resolve, reject, timer, expectBinary: opts.expectBinary });
    rvs.send('brain_request' as any, {
      requestId,
      method: opts.method || 'GET',
      path,
      ...(opts.body !== undefined ? { body: opts.body } : {}),
      ...(opts.bodyBase64 ? { bodyBase64: opts.bodyBase64 } : {}),
      ...(opts.contentType ? { contentType: opts.contentType } : {}),
    });
  });
}

// ── Typen ────────────────────────────────────────────────────────────

export interface MemoryAttachment {
  name: string;
  mime: string;
  size: number;
  path: string;
}

export interface Memory {
  id: string;
  type: string;
  title: string;
  content: string;
  pinned: boolean;
  category: string;
  source: string;
  tags: string[];
  created_at: string;
  updated_at: string;
  conversation_id?: string | null;
  score?: number | null;
  attachments?: MemoryAttachment[];
}

/** OAuth-Service-Status wie aus Brain `/oauth/services` zurueckkommt. */
export interface OAuthServiceStatus {
  service: string;
  configured: boolean;
  authenticated: boolean;
  expiresAt?: number | null;
  expiresInSec?: number | null;
  hasRefresh: boolean;
  scope?: string;
  isDefault: boolean;
}

/** OAuth-App-Config (client_id/scopes/URLs) — client_secret kommt NIE rausgegeben. */
export interface OAuthAppConfig {
  client_id: string;
  has_client_secret: boolean;
  scopes?: string[] | null;
  auth_url?: string | null;
  token_url?: string | null;
}

/** Projekt — Stefans Threading-Konzept im Hauptchat. */
export interface Project {
  id: string;
  name: string;
  description: string;
  status: 'active' | 'ended' | 'archived';
  created_at: number;
  updated_at: number;
  last_activity_at: number;
  turn_count: number;
}

export interface ProjectStatus {
  active_id: string;
  active: Project | null;
  projects: Project[];
}

/** Queue-Status pro Kontext — was gerade arbeitet, was wartet.
 *  Key "__main__" = Hauptchat, sonst project_id. */
export interface QueueContextStatus {
  busy: boolean;
  queue_size: number;
}
export interface ProjectQueueStatus {
  contexts: Record<string, QueueContextStatus>;
}

/** Skill-Manifest wie aus Brain `/skills/list` zurueckkommt. */
export interface Skill {
  name: string;
  description: string;
  execution: string;              // local-venv | local-bin | bash
  entry: string;                  // run.py | run.sh
  args?: any[];                   // [{name, type, required, description}]
  requires?: { pip?: string[]; binaries?: string[] };
  active: boolean;
  created_at?: string;
  updated_at?: string;
  last_used?: string | null;
  use_count?: number;
  version?: string;
  author?: string;                // "aria" | "stefan"
  setup_error?: string;
  // P3: konfigurierbare Werte (API-Keys, IDs etc.) — Stefan setzt sie hier,
  // Skill bekommt sie als CFG_<NAME> ENV. Werte selbst kommen via /config.
  config_schema?: SkillConfigField[];
  // P4: Versions-Historie. Detail-Liste kommt via /versions.
  version_history?: { version_id: string; archived_at?: string; summary?: string }[];
}

export interface SkillConfigField {
  name: string;
  type: 'string' | 'number' | 'boolean' | 'password';
  label?: string;
  secret?: boolean;
  description?: string;
  default?: any;
}

export interface SkillVersion {
  version_id: string;
  archived_at?: string;
  summary?: string;
}

/** Trigger-Manifest wie aus Brain `/triggers/list` zurueckkommt. */
export interface Trigger {
  name: string;
  type: 'timer' | 'watcher' | string;
  active: boolean;
  author?: string;
  message: string;
  fires_at?: string;             // ISO, nur timer
  condition?: string;            // nur watcher
  check_interval_sec?: number;   // nur watcher
  throttle_sec?: number;         // nur watcher
  fire_count?: number;
  last_fired_at?: string | null;
  last_checked_at?: string | null;
  created_at?: string;
  updated_at?: string;
}

// ── Memory CRUD ──────────────────────────────────────────────────────

export const brainApi = {
  /** Einzelne Memory holen (mit allen Feldern inkl. Anhaenge) */
  getMemory(id: string): Promise<Memory> {
    return _send(`/memory/get/${encodeURIComponent(id)}`);
  },

  /** Liste aller Memories, optional nach Type gefiltert. */
  listMemories(opts: { type?: string; limit?: number } = {}): Promise<Memory[]> {
    const qs = _qs({ type: opts.type, limit: opts.limit || 500 });
    return _send(`/memory/list${qs}`);
  },

  /** Volltext-Substring-Suche. */
  searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise<Memory[]> {
    const qs = _qs({
      q,
      type: opts.type,
      include_pinned: opts.includePinned !== false,
      k: opts.k || 50,
    });
    return _send(`/memory/search-text${qs}`);
  },

  /** Semantische Suche (Embedder). */
  searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise<Memory[]> {
    const qs = _qs({
      q,
      type: opts.type,
      include_pinned: opts.includePinned !== false,
      k: opts.k || 10,
      score_threshold: opts.threshold ?? 0.30,
    });
    return _send(`/memory/search${qs}`);
  },

  /** Memory anlegen. */
  saveMemory(body: {
    type: string;
    title: string;
    content: string;
    pinned?: boolean;
    category?: string;
    tags?: string[];
  }): Promise<Memory> {
    return _send('/memory/save', {
      method: 'POST',
      body: { source: 'app', ...body },
    });
  },

  /** Memory aktualisieren (Patch — nur uebergebene Felder werden geaendert). */
  updateMemory(id: string, body: Partial<Pick<Memory, 'title' | 'content' | 'pinned' | 'category' | 'tags'>>): Promise<Memory> {
    return _send(`/memory/update/${encodeURIComponent(id)}`, {
      method: 'PATCH',
      body,
    });
  },

  /** Memory loeschen. */
  deleteMemory(id: string): Promise<{ deleted: string }> {
    return _send(`/memory/delete/${encodeURIComponent(id)}`, {
      method: 'DELETE',
      timeoutMs: 15000,
    });
  },

  // ── Anhaenge ────────────────────────────────────────────────────────

  /** Datei als Anhang an die Memory haengen (Base64-Upload). */
  uploadAttachment(memoryId: string, name: string, base64: string): Promise<Memory> {
    return _send(`/memory/${encodeURIComponent(memoryId)}/attachments`, {
      method: 'POST',
      body: { name, data_base64: base64 },
      timeoutMs: 120000,
    });
  },

  /** Anhang loeschen. */
  deleteAttachment(memoryId: string, filename: string): Promise<Memory> {
    return _send(
      `/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`,
      { method: 'DELETE' },
    );
  },

  /** Anhang-Bytes holen (fuer Vorschau / Download). Liefert Base64. */
  getAttachmentBytes(memoryId: string, filename: string): Promise<{ base64: string; contentType: string }> {
    return _send(
      `/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`,
      { expectBinary: true, timeoutMs: 60000 },
    );
  },

  // ── Triggers ────────────────────────────────────────────────────────

  /** Liste aller Trigger (aktive + inaktive).
   * Brain returnt {triggers: [...]} — wir unwrappen damit der Caller einfach
   * t.sort/filter/map nutzen kann. Ohne das Unwrap warf t.sort() eine
   * TypeError-Exception und der TriggerBrowser blieb leer. */
  listTriggers(): Promise<Trigger[]> {
    return _send('/triggers/list').then((r: any) => Array.isArray(r) ? r : (r?.triggers || []));
  },

  /** Einzelnen Trigger holen (inkl. fire_count, last_fired_at, ...). */
  getTrigger(name: string): Promise<Trigger> {
    return _send(`/triggers/${encodeURIComponent(name)}`);
  },

  /** Verfuegbare Condition-Variablen + Funktionen (fuer Watcher-Editor). */
  getTriggerConditions(): Promise<{ variables: any[]; functions: any[] }> {
    return _send('/triggers/conditions');
  },

  /** Trigger-Logs (last N Feuerungen). */
  getTriggerLogs(name: string, limit: number = 50): Promise<any[]> {
    return _send(`/triggers/${encodeURIComponent(name)}/logs?limit=${limit}`);
  },

  /** Timer anlegen. fires_at = ISO timestamp (UTC). */
  createTimer(body: { name: string; fires_at: string; message: string; author?: string }): Promise<Trigger> {
    return _send('/triggers/timer', {
      method: 'POST',
      body: { author: 'app', ...body },
    });
  },

  /** Watcher anlegen. */
  createWatcher(body: {
    name: string;
    condition: string;
    message: string;
    check_interval_sec?: number;
    throttle_sec?: number;
    author?: string;
  }): Promise<Trigger> {
    return _send('/triggers/watcher', {
      method: 'POST',
      body: { author: 'app', ...body },
    });
  },

  /** Trigger patchen (active/message/condition/throttle/interval/fires_at). */
  updateTrigger(name: string, body: Partial<{
    active: boolean;
    message: string;
    condition: string;
    throttle_sec: number;
    check_interval_sec: number;
    fires_at: string;
  }>): Promise<Trigger> {
    return _send(`/triggers/${encodeURIComponent(name)}`, {
      method: 'PATCH',
      body,
    });
  },

  /** Trigger loeschen. */
  deleteTrigger(name: string): Promise<{ deleted: string }> {
    return _send(`/triggers/${encodeURIComponent(name)}`, {
      method: 'DELETE',
      timeoutMs: 15000,
    });
  },

  // ── Skills ────────────────────────────────────────────────────────

  /** Liste aller Skills (aktive + inaktive). Brain returnt {skills: [...]}. */
  listSkills(): Promise<Skill[]> {
    return _send('/skills/list').then((r: any) => Array.isArray(r) ? r : (r?.skills || []));
  },

  /** Einzelnen Skill holen (inkl. setup_error, last_used, use_count). */
  getSkill(name: string): Promise<Skill> {
    return _send(`/skills/${encodeURIComponent(name)}`);
  },

  /** Skill ausfuehren (mit args als ENV ARG_XXX). Skill-Run kann lange dauern,
   * 5 min Default-Timeout. */
  runSkill(name: string, args: Record<string, any> = {}): Promise<{
    ok: boolean; exit_code: number; stdout: string; stderr: string;
    duration_sec: number; log_path?: string;
  }> {
    return _send('/skills/run', {
      method: 'POST',
      body: { name, args, timeout_sec: 300 },
      timeoutMs: 320000,
    });
  },

  /** Skill-Manifest aendern (description, active, args...). Code-Aenderungen
   * gehen ueber ARIAs eigene skill_update-Tool — die App-UI sollte sie
   * NICHT direkt anbieten (zu fehleranfaellig). */
  updateSkill(name: string, body: Partial<{
    description: string;
    active: boolean;
    args: any[];
    version: string;
  }>): Promise<Skill> {
    return _send(`/skills/${encodeURIComponent(name)}`, {
      method: 'PATCH',
      body,
      timeoutMs: 15000,
    });
  },

  /** Skill loeschen (samt venv + logs). */
  deleteSkill(name: string): Promise<{ deleted: string }> {
    return _send(`/skills/${encodeURIComponent(name)}`, {
      method: 'DELETE',
      timeoutMs: 15000,
    });
  },

  /** Letzte Run-Logs eines Skills. */
  getSkillLogs(name: string, limit: number = 20): Promise<any[]> {
    return _send(`/skills/${encodeURIComponent(name)}/logs?limit=${limit}`)
      .then((r: any) => Array.isArray(r) ? r : (r?.logs || []));
  },

  /** P3: Config-Schema + aktuelle Werte (secret-Felder gemaskt mit '***SET***'). */
  getSkillConfig(name: string): Promise<{ schema: SkillConfigField[]; values: Record<string, any> }> {
    return _send(`/skills/${encodeURIComponent(name)}/config`)
      .then((r: any) => ({ schema: r?.schema || [], values: r?.values || {} }));
  },

  /** P3: Config-Werte komplett ueberschreiben. Werte greifen ab dem naechsten Run. */
  setSkillConfig(name: string, values: Record<string, any>): Promise<{ ok: boolean; values: Record<string, any> }> {
    return _send(`/skills/${encodeURIComponent(name)}/config`, {
      method: 'POST',
      body: { values },
      timeoutMs: 10000,
    });
  },

  /** P4: Liste archivierter Versionen, neueste zuerst. */
  listSkillVersions(name: string): Promise<SkillVersion[]> {
    return _send(`/skills/${encodeURIComponent(name)}/versions`)
      .then((r: any) => r?.versions || []);
  },

  /** P4: Rollback auf eine fruehere Version. Aktueller Stand wird automatisch gesichert. */
  rollbackSkill(name: string, versionId: string): Promise<{ ok: boolean; rolled_back_to: string; safety_snapshot: string }> {
    return _send(`/skills/${encodeURIComponent(name)}/rollback`, {
      method: 'POST',
      body: { version_id: versionId },
      timeoutMs: 60000,  // venv-Rebuild kann dauern
    });
  },

  /** P4: Einzelne Version dauerhaft loeschen. */
  deleteSkillVersion(name: string, versionId: string): Promise<{ ok: boolean; deleted: string }> {
    return _send(`/skills/${encodeURIComponent(name)}/versions/${encodeURIComponent(versionId)}`, {
      method: 'DELETE',
      timeoutMs: 10000,
    });
  },

  // ── OAuth ────────────────────────────────────────────────────────

  /** Liste aller Services mit Auth-Status (configured/authenticated/expires). */
  listOAuthServices(): Promise<{ services: OAuthServiceStatus[] }> {
    return _send('/oauth/services');
  },

  /** Persistierte Provider-Configs (URLs/scopes/client_id, KEIN client_secret). */
  getOAuthApps(): Promise<{ apps: Record<string, OAuthAppConfig>; defaults: string[] }> {
    return _send('/oauth/apps');
  },

  /** Provider-Config setzen/aktualisieren. Leerer client_secret laesst
   * den bestehenden Wert stehen. */
  saveOAuthApp(body: {
    service: string;
    client_id?: string;
    client_secret?: string;
    scopes?: string[];
    auth_url?: string;
    token_url?: string;
  }): Promise<{ ok: boolean; service: string }> {
    return _send('/oauth/apps', {
      method: 'POST',
      body,
      timeoutMs: 15000,
    });
  },

  /** Service-Eintrag komplett entfernen (incl. Token). */
  deleteOAuthApp(service: string): Promise<{ ok: boolean }> {
    return _send(`/oauth/apps/${encodeURIComponent(service)}`, {
      method: 'DELETE',
      timeoutMs: 15000,
    });
  },

  /** Authorize-URL bauen (Brain speichert state, gibt url + redirect_uri zurueck). */
  authorizeOAuth(service: string, scopes?: string[]): Promise<{
    url: string; state: string; redirect_uri: string; service: string;
  }> {
    return _send('/oauth/authorize', {
      method: 'POST',
      body: { service, scopes },
      timeoutMs: 15000,
    });
  },

  /** Token loeschen (lokal — kein Provider-Revoke). */
  revokeOAuth(service: string): Promise<{ ok: boolean }> {
    return _send(`/oauth/${encodeURIComponent(service)}/revoke`, {
      method: 'POST',
      timeoutMs: 15000,
    });
  },

  // ── Projekte ───────────────────────────────────────────────────

  /** Kompletter Status: aktives Projekt + Liste. */
  getProjectStatus(): Promise<ProjectStatus> {
    return _send('/projects/status');
  },

  /** Nur die Liste — fuer Sidebar/Drawer. */
  listProjects(includeArchived: boolean = false): Promise<Project[]> {
    return _send(`/projects/list${includeArchived ? '?include_archived=true' : ''}`)
      .then((r: any) => r?.projects || []);
  },

  /** Neues Projekt anlegen — wird automatisch aktiviert. */
  createProject(body: { name: string; description?: string }): Promise<Project> {
    return _send('/projects/create', {
      method: 'POST',
      body: { description: '', ...body },
    });
  },

  /** Aktives Projekt wechseln. Leerer projectId = Hauptthread. */
  switchProject(projectId: string): Promise<ProjectStatus> {
    return _send('/projects/switch', {
      method: 'POST',
      body: { project_id: projectId },
    });
  },

  /** Projekt als beendet markieren (bleibt sichtbar, aktiv ist dann der Hauptthread). */
  endProject(projectId: string): Promise<Project> {
    return _send(`/projects/${encodeURIComponent(projectId)}/end`, {
      method: 'POST',
    });
  },

  /** Projekt archivieren (verschwindet aus der Default-Liste). */
  archiveProject(projectId: string): Promise<{ id: string; status: string }> {
    return _send(`/projects/${encodeURIComponent(projectId)}/archive`, {
      method: 'POST',
    });
  },

  /** Projekt-Metadaten patchen (name / description). */
  updateProject(projectId: string, patch: Partial<Pick<Project, 'name' | 'description'>>): Promise<Project> {
    return _send(`/projects/${encodeURIComponent(projectId)}`, {
      method: 'PATCH',
      body: patch,
    });
  },

  /** Queue-Status: pro Kontext (project_id oder __main__ fuer Hauptchat)
   *  ob gerade ein Request in Verarbeitung ist + wieviele in der Queue warten.
   *  Wird fuer Status-Dots im Drawer periodisch gepollt. */
  getProjectQueueStatus(): Promise<ProjectQueueStatus> {
    return _send('/projects/queue-status');
  },
};

export default brainApi;
