import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { execa } from 'execa';
import OpenAI from 'openai';

const editSchema = {
  type: 'object',
  additionalProperties: false,
  properties: {
    summary: { type: 'string' },
    files: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        properties: {
          path: { type: 'string' },
          content: { type: 'string' },
        },
        required: ['path', 'content'],
      },
    },
    commands: {
      type: 'array',
      items: { type: 'string' },
    },
    limitations: {
      type: 'array',
      items: { type: 'string' },
    },
  },
  required: ['summary', 'files', 'commands', 'limitations'],
} as const;

type AgentEdit = {
  summary: string;
  files: { path: string; content: string }[];
  commands: string[];
  limitations: string[];
};

const maxContextFiles = 40;
const maxFileBytes = 12_000;

const safeContextFile = (file: string) =>
  !file.includes('node_modules/') &&
  !file.includes('.git/') &&
  !file.includes('dist/') &&
  !file.includes('build/') &&
  !file.includes('.next/') &&
  !file.endsWith('.lock') &&
  !file.endsWith('.png') &&
  !file.endsWith('.jpg') &&
  !file.endsWith('.jpeg') &&
  !file.endsWith('.webp') &&
  !file.endsWith('.gif') &&
  !file.endsWith('.pdf');

const listFiles = async (repoDir: string) => {
  const result = await execa('git', ['ls-files'], {
    cwd: repoDir,
    all: true,
    reject: false,
  });
  return result.all
    .split('\n')
    .map((file) => file.trim())
    .filter(Boolean)
    .filter(safeContextFile);
};

const chooseContextFiles = (files: string[], prompt: string) => {
  const promptWords = new Set(
    prompt
      .toLowerCase()
      .split(/[^a-z0-9]+/)
      .filter((word) => word.length > 3),
  );
  const scored = files.map((file) => {
    const lower = file.toLowerCase();
    const score = [...promptWords].reduce(
      (sum, word) => sum + (lower.includes(word) ? 2 : 0),
      /(readme|package\.json|auth|env|config|route|provider)/i.exec(file)
        ? 3
        : 0,
    );
    return { file, score };
  });
  return scored
    .sort((a, b) => b.score - a.score)
    .slice(0, maxContextFiles)
    .map((item) => item.file);
};

const readContext = async (repoDir: string, files: string[]) => {
  const chunks = [];
  for (const file of files) {
    try {
      const content = await readFile(path.join(repoDir, file), 'utf8');
      chunks.push({
        path: file,
        content:
          content.length > maxFileBytes
            ? `${content.slice(0, maxFileBytes)}\n[truncated]`
            : content,
      });
    } catch {
      // Ignore files that disappeared while context was being gathered.
    }
  }
  return chunks;
};

const parseEdit = (value: string): AgentEdit => {
  const parsed = JSON.parse(value) as AgentEdit;
  if (!Array.isArray(parsed.files)) {
    throw new Error('OpenAI returned an edit without a files array.');
  }
  return parsed;
};

const safePath = (repoDir: string, filePath: string) => {
  const resolved = path.resolve(repoDir, filePath);
  if (!resolved.startsWith(path.resolve(repoDir))) {
    throw new Error(`Refusing to write outside the repository: ${filePath}`);
  }
  return resolved;
};

export const runOpenAiEdit = async (args: {
  repoDir: string;
  apiKey: string;
  model: string;
  reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
  prompt: string;
  secretNames: string[];
  spoonName: string;
  upstreamFullName: string;
  forkFullName: string;
}) => {
  const files = await listFiles(args.repoDir);
  const selectedFiles = chooseContextFiles(files, args.prompt);
  const contextFiles = await readContext(args.repoDir, selectedFiles);
  const response = await new OpenAI({ apiKey: args.apiKey }).responses.create({
    model: args.model,
    store: false,
    reasoning:
      args.reasoningEffort === 'none'
        ? undefined
        : { effort: args.reasoningEffort },
    input: [
      {
        role: 'system',
        content:
          'You are a conservative coding agent working in a fork. Return complete replacement contents only for files that must change. Keep the diff minimal. Do not include secrets. Do not claim commands passed unless they are listed for the worker to run. If the context is insufficient, make the safest small change and describe limitations.',
      },
      {
        role: 'user',
        content: JSON.stringify(
          {
            task: args.prompt,
            spoon: args.spoonName,
            upstream: args.upstreamFullName,
            fork: args.forkFullName,
            availableSecretNames: args.secretNames,
            repositoryFiles: files.slice(0, 500),
            contextFiles,
          },
          null,
          2,
        ),
      },
    ],
    text: {
      format: {
        type: 'json_schema',
        name: 'spoon_agent_file_edits',
        strict: true,
        schema: editSchema,
      },
    },
  });
  const edit = parseEdit(response.output_text);
  for (const file of edit.files) {
    const target = safePath(args.repoDir, file.path);
    await mkdir(path.dirname(target), { recursive: true });
    await writeFile(target, file.content);
  }
  return edit;
};
