// Pure logic for the Brain Dashboard. Browser-safe ES module (no node: APIs)
// so it loads both in the browser and under Vitest's node environment.

export function normalizeEpisode(raw) {
  const v2 = raw.schema_version === 2;
  const pr = raw.primary_rationale || {};
  const events = Array.isArray(raw.events) ? raw.events : [];
  const tools = {};
  for (const ev of events) {
    if (ev.kind === 'tool_summary' && ev.counts) {
      for (const [k, n] of Object.entries(ev.counts)) tools[k] = (tools[k] || 0) + n;
    }
  }
  const started = raw.timestamps?.started_at || null;
  const ended = raw.timestamps?.ended_at || null;
  return {
    schemaVersion: v2 ? 2 : 1,
    taskId: raw.task_id || null,
    taskRef: raw.task_ref || raw.task_id || null,
    startedAt: started,
    endedAt: ended,
    durationMs: started && ended ? Date.parse(ended) - Date.parse(started) : null,
    pathType: raw.path_type || null,
    outcome: raw.outcome || 'unknown',
    promptSignal: v2 ? raw.prompt_signal || null : null,
    decisionProvenance: v2 ? raw.decision_provenance || null : null,
    environment: v2 ? raw.environment || null : null,
    taskSize: v2 ? raw.task_size || null : null,
    taskClassification: pr.task_classification || null,
    nodeChosen: pr.node_chosen || null,
    hardFloor: pr.hard_floor || { invoked: false, rules: [] },
    skills: events.filter((e) => e.kind === 'skill_invoked').map((e) => e.skill),
    tools,
    errorCount: events.filter((e) => e.kind === 'error').length,
    retryCount: events.filter((e) => e.kind === 'retry').length,
    interruptCount: events.filter((e) => e.kind === 'interrupt').length,
    events,
    raw,
  };
}

// episode skill name → automation-graph node id (see tools/observer-known-nodes.txt
// for the routable vocabulary; only skills that have a graph node are listed).
export const SKILL_TO_NODE = {
  brainstorming: 'sk_brainstorm',
  'writing-plans': 'sk_wplans',
  'executing-plans': 'sk_eplans',
  'subagent-driven-development': 'sk_subagent',
  'test-driven-development': 'sk_tdd',
  'systematic-debugging': 'sk_debug',
  'verification-before-completion': 'sk_verify',
  'requesting-code-review': 'sk_coderev',
  'using-git-worktrees': 'sk_worktree',
  'finishing-a-development-branch': 'sk_pr',
  'writing-skills': 'sk_wskills',
  'discovery-interview': 'discovery_interview',
  'audit-portal': 'sk_audit_portal',
  regression: 'sk_regression',
  'process-modeling': 'process_modeling',
  'process-analysis': 'process_analysis',
  ccpm: 'ccpm',
  'security-review': 'sk_security_review',
  'claude-md-management': 'claude_md_mgmt',
};

// mcp__<server>__<tool> → automation-graph node id.
export const MCP_SERVER_TO_NODE = {
  github: 'mcp_gh',
  playwright: 'mcp_pw',
  'laravel-boost': 'mcp_boost',
  redis: 'mcp_redis',
  sentry: 'mcp_sentry',
  semgrep: 'mcp_semgrep',
  openapi: 'mcp_openapi',
  magic: 'mcp_21st',
  'universal-icons': 'mcp_icons',
};

// "superpowers:systematic-debugging" → "systematic-debugging"
function skillBase(name) {
  const s = String(name || '');
  return s.includes(':') ? s.split(':').pop() : s;
}

// Returns { nodeIds: string[], signals: number, attributed: number }.
// A "signal" is an episode datum that names a routable node (a skill id or an
// mcp__ tool). Builtin Claude tools are not signals.
export function attributeNodes(episode) {
  const ids = new Set();
  let signals = 0;
  let attributed = 0;
  const consider = (nodeId) => {
    signals++;
    if (nodeId) {
      ids.add(nodeId);
      attributed++;
    }
  };
  if (episode.nodeChosen && episode.nodeChosen !== 'direct') {
    consider(SKILL_TO_NODE[skillBase(episode.nodeChosen)]);
  }
  for (const s of episode.skills) consider(SKILL_TO_NODE[skillBase(s)]);
  for (const toolName of Object.keys(episode.tools)) {
    const m = /^mcp__(.+?)__/.exec(toolName);
    if (m) consider(MCP_SERVER_TO_NODE[m[1]]);
  }
  return { nodeIds: [...ids], signals, attributed };
}

// Groups episodes by taskRef. Each group's episodes are sorted newest-first;
// groups are ordered by their newest episode, newest group first.
export function groupBySession(episodes) {
  const byRef = new Map();
  for (const e of episodes) {
    const key = e.taskRef || e.taskId || 'unknown';
    if (!byRef.has(key)) byRef.set(key, []);
    byRef.get(key).push(e);
  }
  const groups = [...byRef.entries()].map(([taskRef, eps]) => {
    eps.sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
    return { taskRef, episodes: eps, newest: eps[0]?.startedAt || '' };
  });
  groups.sort((a, b) => String(b.newest).localeCompare(String(a.newest)));
  return groups;
}

// filter: { classification?, outcome?, pathType?, withErrors?, dateFrom?, dateTo? }
export function filterEpisodes(episodes, filter = {}) {
  return episodes.filter((e) => {
    if (filter.classification && e.taskClassification !== filter.classification) return false;
    if (filter.outcome && e.outcome !== filter.outcome) return false;
    if (filter.pathType && e.pathType !== filter.pathType) return false;
    if (filter.withErrors && e.errorCount === 0 && e.retryCount === 0) return false;
    if (filter.dateFrom && String(e.startedAt) < filter.dateFrom) return false;
    if (filter.dateTo && String(e.startedAt) > filter.dateTo) return false;
    return true;
  });
}

// Three honest layers (spec §6):
//  design      — the dashed conflict edges (fact, from topology)
//  friction    — node id → count of errored/retried episodes attributed to it
//  correlation — errored episodes that span both ends of a design-conflict edge
export function inferConflicts(episodes, edges) {
  const design = edges.filter((e) => e.dashes === true);
  const friction = {};
  const correlation = [];
  for (const e of episodes) {
    if (e.errorCount === 0 && e.retryCount === 0) continue;
    const ids = attributeNodes(e).nodeIds;
    for (const id of ids) friction[id] = (friction[id] || 0) + 1;
    if (e.errorCount > 0) {
      for (const edge of design) {
        if (ids.includes(edge.from) && ids.includes(edge.to)) {
          correlation.push({ episode: e.taskId, pair: [edge.from, edge.to], conflict: edge.title || '' });
        }
      }
    }
  }
  return { design, friction, correlation };
}

// Aggregates a list of episodes into dashboard metrics.
export function aggregate(episodes) {
  const nodeHeat = {};
  const pathType = {};
  const outcome = {};
  const classification = {};
  const economy = {};
  let totalErrors = 0;
  let totalRetries = 0;
  let redirects = 0;
  for (const e of episodes) {
    for (const id of attributeNodes(e).nodeIds) nodeHeat[id] = (nodeHeat[id] || 0) + 1;
    if (e.pathType) pathType[e.pathType] = (pathType[e.pathType] || 0) + 1;
    outcome[e.outcome] = (outcome[e.outcome] || 0) + 1;
    if (e.taskClassification) classification[e.taskClassification] = (classification[e.taskClassification] || 0) + 1;
    const lvl = e.environment ? e.environment.economy_level : null;
    const key = lvl == null ? 'n/a' : String(lvl);
    economy[key] = (economy[key] || 0) + 1;
    totalErrors += e.errorCount;
    totalRetries += e.retryCount;
    if (e.decisionProvenance && e.decisionProvenance.kind === 'user_directed_method') redirects++;
  }
  return {
    nodeHeat,
    pathType,
    outcome,
    classification,
    economy,
    totalErrors,
    totalRetries,
    redirectRate: episodes.length ? redirects / episodes.length : 0,
    count: episodes.length,
  };
}

export function parseEpisodes(text) {
  const episodes = [];
  let skipped = 0;
  for (const line of String(text).split('\n')) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    let raw;
    try {
      raw = JSON.parse(trimmed);
    } catch {
      skipped++;
      continue;
    }
    if (!raw || typeof raw !== 'object' || raw.observer_error) {
      skipped++;
      continue;
    }
    episodes.push(normalizeEpisode(raw));
  }
  return { episodes, skipped };
}
