/**
 * File-Based Agent I/O
 *
 * Writes context as input files before agent spawn and reads output files after completion.
 * Uses YAML frontmatter (gray-matter) for structured metadata and markdown bodies.
 *
 * Input: .cw/input/ — written by system before spawn
 * Output: .cw/output/ — written by agent during execution
 */

import { existsSync } from 'node:fs';
import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import matter from 'gray-matter';
import { nanoid } from 'nanoid';
import { tiptapJsonToMarkdown } from './content-serializer.js';
import type { AgentInputContext } from './types.js';

// Re-export for convenience
export type { AgentInputContext } from './types.js';

// =============================================================================
// TYPES
// =============================================================================

export interface WriteInputFilesOptions extends AgentInputContext {
  agentWorkdir: string;
}

export interface ParsedSummary {
  body: string;
  filesModified?: string[];
}

export interface ParsedPhaseFile {
  id: string;
  title: string;
  dependencies: string[];
  body: string;
  action?: 'create' | 'update' | 'delete';
}

export interface ParsedTaskFile {
  id: string;
  title: string;
  category: string;
  type: string;
  dependencies: string[];
  body: string;
  action?: 'create' | 'update' | 'delete';
  phaseId?: string;
  parentTaskId?: string;
}

export interface ParsedDecisionFile {
  id: string;
  topic: string;
  decision: string;
  reason: string;
  body: string;
}

export interface ParsedPageFile {
  pageId: string;
  title: string;
  summary: string;
  body: string;
  action?: 'create' | 'update' | 'delete';
}

// =============================================================================
// ID GENERATION
// =============================================================================

export function generateId(): string {
  return nanoid();
}

// =============================================================================
// INPUT FILE WRITING
// =============================================================================

function formatFrontmatter(data: Record<string, unknown>, body: string = ''): string {
  const lines: string[] = ['---'];
  for (const [key, value] of Object.entries(data)) {
    if (value === undefined || value === null) continue;
    if (Array.isArray(value)) {
      if (value.length === 0) {
        lines.push(`${key}: []`);
      } else {
        lines.push(`${key}:`);
        for (const item of value) {
          lines.push(`  - ${String(item)}`);
        }
      }
    } else if (value instanceof Date) {
      lines.push(`${key}: "${value.toISOString()}"`);
    } else if (typeof value === 'string' && (value.includes('\n') || value.includes(':'))) {
      lines.push(`${key}: ${JSON.stringify(value)}`);
    } else {
      lines.push(`${key}: ${String(value)}`);
    }
  }
  lines.push('---');
  if (body) {
    lines.push('');
    lines.push(body);
  }
  return lines.join('\n') + '\n';
}

export async function writeInputFiles(options: WriteInputFilesOptions): Promise<void> {
  const inputDir = join(options.agentWorkdir, '.cw', 'input');
  await mkdir(inputDir, { recursive: true });

  // Write expected working directory marker for verification
  await writeFile(
    join(inputDir, '../expected-pwd.txt'),
    options.agentWorkdir,
    'utf-8'
  );

  const manifestFiles: string[] = [];

  // Collect all file writes, then flush in parallel
  const writes: Array<{ path: string; content: string }> = [];

  if (options.initiative) {
    const ini = options.initiative;
    const content = formatFrontmatter(
      {
        id: ini.id,
        name: ini.name,
        status: ini.status,
        branch: ini.branch,
      },
      '',
    );
    writes.push({ path: join(inputDir, 'initiative.md'), content });
    manifestFiles.push('initiative.md');
  }

  if (options.pages && options.pages.length > 0) {
    await mkdir(join(inputDir, 'pages'), { recursive: true });

    for (const page of options.pages) {
      let bodyMarkdown = '';
      if (page.content) {
        try {
          const parsed = JSON.parse(page.content);
          bodyMarkdown = tiptapJsonToMarkdown(parsed);
        } catch {
          // Invalid JSON content — skip
        }
      }

      const content = formatFrontmatter(
        {
          title: page.title,
          parentPageId: page.parentPageId,
          sortOrder: page.sortOrder,
        },
        bodyMarkdown,
      );
      const filename = `pages/${page.id}.md`;
      writes.push({ path: join(inputDir, 'pages', `${page.id}.md`), content });
      manifestFiles.push(filename);
    }
  }

  if (options.phase) {
    const ph = options.phase;
    let bodyMarkdown = '';
    if (ph.content) {
      try {
        bodyMarkdown = tiptapJsonToMarkdown(JSON.parse(ph.content));
      } catch {
        // Invalid JSON content — skip
      }
    }
    const content = formatFrontmatter(
      {
        id: ph.id,
        name: ph.name,
        status: ph.status,
      },
      bodyMarkdown,
    );
    writes.push({ path: join(inputDir, 'phase.md'), content });
    manifestFiles.push('phase.md');
  }

  if (options.task) {
    const t = options.task;
    const content = formatFrontmatter(
      {
        id: t.id,
        name: t.name,
        category: t.category,
        type: t.type,
        priority: t.priority,
        status: t.status,
      },
      t.description ?? '',
    );
    writes.push({ path: join(inputDir, 'task.md'), content });
    manifestFiles.push('task.md');
  }

  // Write read-only context directories
  const contextFiles: string[] = [];

  if (options.phases && options.phases.length > 0) {
    await mkdir(join(inputDir, 'context', 'phases'), { recursive: true });

    for (const ph of options.phases) {
      let bodyMarkdown = '';
      if (ph.content) {
        try {
          bodyMarkdown = tiptapJsonToMarkdown(JSON.parse(ph.content));
        } catch {
          // Invalid JSON content — skip
        }
      }
      const content = formatFrontmatter(
        {
          id: ph.id,
          name: ph.name,
          status: ph.status,
          dependsOn: ph.dependsOn ?? [],
        },
        bodyMarkdown,
      );
      const filename = `context/phases/${ph.id}.md`;
      writes.push({ path: join(inputDir, 'context', 'phases', `${ph.id}.md`), content });
      contextFiles.push(filename);
    }
  }

  if (options.tasks && options.tasks.length > 0) {
    await mkdir(join(inputDir, 'context', 'tasks'), { recursive: true });

    for (const t of options.tasks) {
      const content = formatFrontmatter(
        {
          id: t.id,
          name: t.name,
          phaseId: t.phaseId,
          parentTaskId: t.parentTaskId,
          category: t.category,
          type: t.type,
          priority: t.priority,
          status: t.status,
          summary: t.summary,
        },
        t.description ?? '',
      );
      const filename = `context/tasks/${t.id}.md`;
      writes.push({ path: join(inputDir, 'context', 'tasks', `${t.id}.md`), content });
      contextFiles.push(filename);
    }
  }

  // Write context index — groups tasks by phaseId so agents can look up relevant files
  // without bulk-reading every context file
  if (options.tasks && options.tasks.length > 0) {
    const tasksByPhase: Record<string, Array<{ file: string; id: string; name: string; status: string }>> = {};
    for (const t of options.tasks) {
      const phaseId = t.phaseId ?? '_unassigned';
      if (!tasksByPhase[phaseId]) tasksByPhase[phaseId] = [];
      tasksByPhase[phaseId].push({
        file: `context/tasks/${t.id}.md`,
        id: t.id,
        name: t.name,
        status: t.status,
      });
    }
    await mkdir(join(inputDir, 'context'), { recursive: true });
    writes.push({
      path: join(inputDir, 'context', 'index.json'),
      content: JSON.stringify({ tasksByPhase }, null, 2) + '\n',
    });
  }

  // Flush all file writes in parallel — yields the event loop between I/O ops
  await Promise.all(writes.map(w => writeFile(w.path, w.content, 'utf-8')));

  // Write manifest last (after all files exist)
  await writeFile(
    join(inputDir, 'manifest.json'),
    JSON.stringify({
      files: manifestFiles,
      contextFiles,
      agentId: options.agentId ?? null,
      agentName: options.agentName ?? null,
    }) + '\n',
    'utf-8',
  );
}

// =============================================================================
// ERRAND INPUT FILE WRITING
// =============================================================================

export async function writeErrandManifest(options: {
  agentWorkdir: string;
  errandId: string;
  description: string;
  branch: string;
  projectName: string;
  agentId: string;
  agentName: string;
}): Promise<void> {
  await mkdir(join(options.agentWorkdir, '.cw', 'input'), { recursive: true });

  // Write errand.md first (before manifest.json)
  const errandMdContent = formatFrontmatter({
    id: options.errandId,
    description: options.description,
    branch: options.branch,
    project: options.projectName,
  });
  await writeFile(join(options.agentWorkdir, '.cw', 'input', 'errand.md'), errandMdContent, 'utf-8');

  // Write manifest.json last (after all other files exist)
  await writeFile(
    join(options.agentWorkdir, '.cw', 'input', 'manifest.json'),
    JSON.stringify({
      errandId: options.errandId,
      agentId: options.agentId,
      agentName: options.agentName,
      mode: 'errand',
    }) + '\n',
    'utf-8',
  );

  // Write expected-pwd.txt
  await writeFile(
    join(options.agentWorkdir, '.cw', 'expected-pwd.txt'),
    options.agentWorkdir,
    'utf-8',
  );
}

// =============================================================================
// OUTPUT FILE READING
// =============================================================================

export async function readFrontmatterFile(filePath: string): Promise<{ data: Record<string, unknown>; body: string } | null> {
  try {
    const raw = await readFile(filePath, 'utf-8');
    const parsed = matter(raw);
    return { data: parsed.data as Record<string, unknown>, body: parsed.content.trim() };
  } catch {
    return null;
  }
}

async function readFrontmatterDir<T>(
  dirPath: string,
  mapper: (data: Record<string, unknown>, body: string, filename: string) => T | null,
): Promise<T[]> {
  if (!existsSync(dirPath)) return [];

  const results: T[] = [];
  try {
    const entries = await readdir(dirPath);
    for (const entry of entries) {
      if (!entry.endsWith('.md')) continue;
      const filePath = join(dirPath, entry);
      const parsed = await readFrontmatterFile(filePath);
      if (!parsed) continue;
      const mapped = mapper(parsed.data, parsed.body, entry);
      if (mapped) results.push(mapped);
    }
  } catch {
    // Directory read error — return empty
  }
  return results;
}

export async function readSummary(agentWorkdir: string): Promise<ParsedSummary | null> {
  const filePath = join(agentWorkdir, '.cw', 'output', 'SUMMARY.md');
  const parsed = await readFrontmatterFile(filePath);
  if (!parsed) return null;

  const filesModified = parsed.data.files_modified;
  return {
    body: parsed.body,
    filesModified: Array.isArray(filesModified) ? filesModified.map(String) : undefined,
  };
}

export async function readPhaseFiles(agentWorkdir: string): Promise<ParsedPhaseFile[]> {
  const dirPath = join(agentWorkdir, '.cw', 'output', 'phases');
  return readFrontmatterDir(dirPath, (data, body, filename) => {
    const id = filename.replace(/\.md$/, '');
    const deps = Array.isArray(data.dependencies) ? data.dependencies.map(String) : [];
    const action = String(data.action ?? 'create') as 'create' | 'update' | 'delete';
    return {
      id,
      title: String(data.title ?? ''),
      dependencies: deps,
      body,
      action,
    };
  });
}

export async function readTaskFiles(agentWorkdir: string): Promise<ParsedTaskFile[]> {
  const dirPath = join(agentWorkdir, '.cw', 'output', 'tasks');
  return readFrontmatterDir(dirPath, (data, body, filename) => {
    const id = filename.replace(/\.md$/, '');
    const deps = Array.isArray(data.dependencies) ? data.dependencies.map(String) : [];
    const action = String(data.action ?? 'create') as 'create' | 'update' | 'delete';
    return {
      id,
      title: String(data.title ?? ''),
      category: String(data.category ?? 'execute'),
      type: String(data.type ?? 'auto'),
      dependencies: deps,
      body,
      action,
      phaseId: data.phaseId ? String(data.phaseId) : undefined,
      parentTaskId: data.parentTaskId ? String(data.parentTaskId) : undefined,
    };
  });
}

export async function readDecisionFiles(agentWorkdir: string): Promise<ParsedDecisionFile[]> {
  const dirPath = join(agentWorkdir, '.cw', 'output', 'decisions');
  return readFrontmatterDir(dirPath, (data, body, filename) => {
    const id = filename.replace(/\.md$/, '');
    return {
      id,
      topic: String(data.topic ?? ''),
      decision: String(data.decision ?? ''),
      reason: String(data.reason ?? ''),
      body,
    };
  });
}

export interface ParsedCommentResponse {
  commentId: string;
  body: string;
  resolved?: boolean;
}

export async function readCommentResponses(agentWorkdir: string): Promise<ParsedCommentResponse[]> {
  const filePath = join(agentWorkdir, '.cw', 'output', 'comment-responses.json');
  try {
    const raw = await readFile(filePath, 'utf-8');
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed
      .filter((entry: unknown) => {
        if (typeof entry !== 'object' || entry === null) return false;
        const e = entry as Record<string, unknown>;
        return typeof e.commentId === 'string' && typeof e.body === 'string';
      })
      .map((entry: Record<string, unknown>) => ({
        commentId: String(entry.commentId),
        body: String(entry.body),
        resolved: typeof entry.resolved === 'boolean' ? entry.resolved : undefined,
      }));
  } catch {
    return [];
  }
}

export async function readPageFiles(agentWorkdir: string): Promise<ParsedPageFile[]> {
  const dirPath = join(agentWorkdir, '.cw', 'output', 'pages');
  return readFrontmatterDir(dirPath, (data, body, filename) => {
    const pageId = filename.replace(/\.md$/, '');
    const action = String(data.action ?? 'create') as 'create' | 'update' | 'delete';
    return {
      pageId,
      title: String(data.title ?? ''),
      summary: String(data.summary ?? ''),
      body,
      action,
    };
  });
}
