// src/engine/reflection/load-inputs.ts
import { readFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { logger } from '../../logger.js';
import { bodyRevision } from './revisions.js';
import type { Repository } from '../../db/repository.js';
import type { ReflectionInput } from './types.js';
import { userMemoryDir, userPiecesDir } from '../../user-folder/paths.js';
import { listMemoryEntries, readMemoryIndexFromDir } from '../../user-folder/memory.js';
import { summarizeActivityLog, type ActivityEvent } from './activity-summarizer.js';
import { EVENT_LOG_FILE } from '../../progress/event-log.js';

export interface LoadInputsArgs {
  originalJobId: string;
  userId: string;
  pieceName: string;
  outcome: 'succeeded' | 'failed' | 'aborted';
}

export interface LoadInputsContext {
  dataDir: string;             // root for data/users/{userId}/...
  builtinPiecesDir: string;    // typically "pieces"
  activityLogMaxBytes: number;
}

export async function loadReflectionInputs(
  repo: Repository,
  args: LoadInputsArgs,
  ctx: LoadInputsContext,
): Promise<ReflectionInput> {
  const job = await repo.getJob(args.originalJobId);
  if (!job) throw new Error(`originalJob not found: ${args.originalJobId}`);

  const taskId = extractLocalTaskId(job.repo);
  const localTask = taskId != null ? await repo.getLocalTask(taskId) : null;

  // Post-completion comments: only those created after the job's updatedAt.
  const completionCutoff = job.updatedAt ?? job.createdAt;
  const allComments = taskId != null ? await repo.listLocalTaskComments(taskId) : [];
  const comments = allComments.filter(c => c.createdAt > completionCutoff);

  // Activity events → compressed summary
  const events = loadActivityEvents(job);
  const activityLogSummary = summarizeActivityLog(events, ctx.activityLogMaxBytes);

  // Memory: load entries and build observedRevisions map (name → sha1(body))
  const memDir = userMemoryDir(ctx.dataDir, args.userId);
  const memoryIndex = readMemoryIndexFromDir(memDir) ?? '';
  const memoryEntries = listMemoryEntries(memDir);
  const observedRevisions: Record<string, string> = {};
  for (const e of memoryEntries) {
    observedRevisions[e.name] = bodyRevision(e.body);
  }

  // Piece YAML: prefer custom (per-user) override, fall back to builtin
  const customPiecePath = join(userPiecesDir(ctx.dataDir, args.userId), `${args.pieceName}.yaml`);
  const builtinPiecePath = join(ctx.builtinPiecesDir, `${args.pieceName}.yaml`);
  let pieceYaml = '';
  let pieceSource: 'builtin' | 'custom' = 'builtin';
  if (existsSync(customPiecePath)) {
    pieceYaml = readFileSync(customPiecePath, 'utf-8');
    pieceSource = 'custom';
  } else if (existsSync(builtinPiecePath)) {
    pieceYaml = readFileSync(builtinPiecePath, 'utf-8');
  }

  // Result text: extract the most useful outcome text for the LLM
  const resultText = extractResultText(job, events);

  return {
    originalJobId: args.originalJobId,
    userId: args.userId,
    pieceName: args.pieceName,
    pieceSource,
    outcome: args.outcome,
    taskTitle: localTask?.title ?? '',
    taskBody: localTask?.body ?? job.instruction,
    activityLogSummary,
    postCompletionComments: comments.map(c => ({
      author: c.author,
      body: c.body,
      createdAt: c.createdAt,
    })),
    feedback: {
      rating: localTask?.feedbackRating ?? null,
      comment: localTask?.feedbackComment ?? null,
      tags: localTask?.feedbackTags ?? [],
    },
    resultText,
    observedRevisions,
    memoryIndex,
    memoryEntries,
    pieceYaml,
  };
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function extractLocalTaskId(repo: string): number | null {
  const m = /^local\/task-(\d+)$/.exec(repo);
  return m ? Number(m[1]) : null;
}

/**
 * Load activity events from the job's event log (logs/events.jsonl inside the
 * worktree path). Returns an empty array when the file is absent or the job
 * has no workspace.
 */
function loadActivityEvents(job: { worktreePath: string | null }): ActivityEvent[] {
  if (!job.worktreePath) return [];

  const logFile = join(job.worktreePath, EVENT_LOG_FILE);
  let raw: string;
  try {
    raw = readFileSync(logFile, 'utf-8');
  } catch {
    return [];
  }

  const events: ActivityEvent[] = [];
  for (const line of raw.split('\n')) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    try {
      const obj = JSON.parse(trimmed) as Record<string, unknown>;
      // Map event-log schema to ActivityEvent shape expected by summarizer.
      const ev: ActivityEvent = {
        type: typeof obj['kind'] === 'string' ? obj['kind'] : String(obj['type'] ?? ''),
        ts: typeof obj['ts'] === 'string' ? obj['ts'] : undefined,
      };
      // tool_call / tool_result events carry payload under `data`
      const data = obj['data'] && typeof obj['data'] === 'object'
        ? (obj['data'] as Record<string, unknown>)
        : {};
      if (ev.type === 'tool_call') {
        ev.tool = typeof data['tool'] === 'string' ? data['tool'] : undefined;
        ev.args = data['args'];
      } else if (ev.type === 'tool_result') {
        ev.tool = typeof data['tool'] === 'string' ? data['tool'] : undefined;
        ev.result = data['result'];
      } else if (ev.type === 'tool_error') {
        ev.tool = typeof data['tool'] === 'string' ? data['tool'] : undefined;
        ev.error = typeof data['error'] === 'string' ? data['error'] : String(data['error'] ?? '');
      } else if (ev.type === 'transition') {
        ev.from = typeof data['from'] === 'string' ? data['from'] : undefined;
        ev.to = typeof data['to'] === 'string' ? data['to'] : undefined;
        ev.reason = typeof data['reason'] === 'string' ? data['reason'] : undefined;
      } else if (ev.type === 'system_warning' || ev.type === 'system_error') {
        ev.reason = typeof data['reason'] === 'string' ? data['reason'] : undefined;
      }
      events.push(ev);
    } catch {
      // skip malformed line
    }
  }
  return events;
}

/**
 * Best-effort: extract a human-readable result/outcome string from the job.
 *
 * Priority:
 *   - For `failed` jobs: job.errorSummary
 *   - For `aborted` jobs: search the event log for the last complete() tool
 *     call with an abort_reason
 *   - For `succeeded` jobs: search the event log for the latest complete()
 *     tool call result
 */
function extractResultText(
  job: { errorSummary: string | null; status: string },
  events: ActivityEvent[],
): string {
  if (job.status === 'failed' && job.errorSummary) {
    return job.errorSummary;
  }

  // Scan events in reverse for the last complete() tool_call
  for (let i = events.length - 1; i >= 0; i--) {
    const ev = events[i]!;
    if (ev.type === 'tool_call' && ev.tool === 'complete' && ev.args) {
      const args = ev.args as Record<string, unknown>;
      if (typeof args['result'] === 'string') return args['result'];
      if (typeof args['abort_reason'] === 'string') return args['abort_reason'];
      if (typeof args['missing_info'] === 'string') return args['missing_info'];
      return JSON.stringify(args).slice(0, 500);
    }
  }

  // Last fallback: errorSummary if present
  return job.errorSummary ?? '';
}
