import { readdir, mkdir, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { itemsDir, OFFICER_ITEMS_DIR } from '../../data-path';

// File-backed task store. Every task is a directory under $OFFICER_ITEMS_DIR/tasks/<dirName>/ with a
// TASK.md (metadata + prose body) and, for script-mode tasks, a sibling implementation file. There are
// no scope tiers and no database — the directory name is the task's identity.

const YAML = (Bun as unknown as { YAML: { parse(input: string): unknown } }).YAML;

export type TaskFrontmatter = {
  name: string;
  description: string | null;
  category: string | null; // groups the task into a context-menu submenu; null falls back to 'Other'
  version: number;
  mode: string;
  // How the task runs: false = background job, true = ephemerally in the modal, 'ask' = the modal
  // offers the choice per run.
  inline: boolean | 'ask';
  language: string | null;
  args: string[] | null;
  tags: string[] | null;
  tools: string[] | null;
  skills: string[] | null;
  inputs: unknown;
  outputs: unknown;
  dependencies: unknown;
  config: unknown;
  trigger: unknown;
};

export type TaskRecord = TaskFrontmatter & {
  dirName: string;
  body: string;
  implementation: string | null;
  filePath: string;
};

export type TaskSummary = {
  dirName: string;
  name: string;
  description: string | null;
  category: string | null;
  mode: string;
  inline: boolean | 'ask';
  version: number;
  trigger: unknown;
};

// Frontmatter keys, in write order. These mirror the old `tasks` table columns 1:1.
const FM_KEYS = [
  'name',
  'description',
  'category',
  'version',
  'mode',
  'inline',
  'language',
  'args',
  'tags',
  'tools',
  'skills',
  'inputs',
  'outputs',
  'dependencies',
  'config',
  'trigger',
] as const;

// Script implementation lives in a sibling file named by language, matching the marketplace registry.
const IMPL_FILE: Record<string, string> = {
  bash: 'run.sh',
  python: 'run.py',
  typescript: 'index.ts',
  javascript: 'index.js',
};

const implFileName = (language: string | null) => IMPL_FILE[language ?? 'bash'] ?? 'run.sh';

const tasksRoot = () => itemsDir('tasks');
const taskDir = (dirName: string) => join(tasksRoot(), dirName);
const taskFile = (dirName: string) => join(taskDir(dirName), 'TASK.md');

const asArray = (v: unknown): string[] | null => (Array.isArray(v) ? v.map(String) : null);

export const slugify = (name: string) =>
  name
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/[^a-z0-9-]/g, '');

function parseTaskMd(raw: string): { fm: TaskFrontmatter; body: string } {
  const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
  const body = match ? match[2]! : raw;
  const yaml = match ? match[1]! : '';

  let parsed: Record<string, unknown> = {};
  if (yaml.trim()) {
    try {
      parsed = (YAML.parse(yaml) as Record<string, unknown>) ?? {};
    } catch {
      parsed = {};
    }
  }

  const versionRaw = parsed.version;
  const version = typeof versionRaw === 'number' ? versionRaw : Number(versionRaw) || 1;

  return {
    fm: {
      name: parsed.name == null ? '' : String(parsed.name),
      description: parsed.description == null ? null : String(parsed.description),
      category: parsed.category == null ? null : String(parsed.category),
      version,
      mode: parsed.mode == null ? 'agentic' : String(parsed.mode),
      inline: parsed.inline === 'ask' ? 'ask' : parsed.inline === true,
      language: parsed.language == null ? null : String(parsed.language),
      args: asArray(parsed.args),
      tags: asArray(parsed.tags),
      tools: asArray(parsed.tools),
      skills: asArray(parsed.skills),
      inputs: parsed.inputs ?? null,
      outputs: parsed.outputs ?? null,
      dependencies: parsed.dependencies ?? null,
      config: parsed.config ?? null,
      trigger: parsed.trigger ?? null,
    },
    body,
  };
}

// Serialize as JSON-flow YAML: every value is JSON.stringify'd, which is valid YAML and round-trips
// losslessly. Bun.YAML.parse also accepts hand-written multi-line YAML, so files stay editable.
function buildTaskMd(fm: Partial<TaskFrontmatter>, body: string): string {
  const lines: string[] = ['---'];
  for (const key of FM_KEYS) {
    const val = (fm as Record<string, unknown>)[key];
    if (val === undefined || val === null) continue;
    if (Array.isArray(val) && val.length === 0) continue;
    lines.push(`${key}: ${JSON.stringify(val)}`);
  }
  lines.push('---', '');
  lines.push(body ?? '');
  return lines.join('\n');
}

async function readImplementation(dirName: string, language: string | null): Promise<string | null> {
  const implPath = join(taskDir(dirName), implFileName(language));
  const file = Bun.file(implPath);
  return (await file.exists()) ? file.text() : null;
}

// categories.yaml at the root of the items store lists the task categories in the order they should
// appear in the file browser's Run Task submenu. It is optional and purely presentational: the
// platform never decides a task's category, only how known ones sort. A missing, empty or malformed
// file leaves everything to alphabetical ordering.
export async function readCategoryOrder(): Promise<string[]> {
  const file = Bun.file(join(OFFICER_ITEMS_DIR, 'categories.yaml'));
  if (!(await file.exists())) return [];
  try {
    const parsed = YAML.parse(await file.text());
    if (!Array.isArray(parsed)) return [];
    return parsed
      .filter((entry): entry is string => typeof entry === 'string')
      .map((entry) => entry.trim())
      .filter(Boolean);
  } catch {
    return [];
  }
}

export async function listTasks(): Promise<TaskSummary[]> {
  let entries;
  try {
    entries = await readdir(tasksRoot(), { withFileTypes: true });
  } catch {
    return [];
  }

  const summaries: TaskSummary[] = [];
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const file = Bun.file(taskFile(entry.name));
    if (!(await file.exists())) continue;
    const { fm } = parseTaskMd(await file.text());
    summaries.push({
      dirName: entry.name,
      name: fm.name || entry.name,
      description: fm.description,
      category: fm.category,
      mode: fm.mode,
      inline: fm.inline,
      version: fm.version,
      trigger: fm.trigger ?? [],
    });
  }

  summaries.sort((a, b) => a.name.localeCompare(b.name));
  return summaries;
}

export async function getTaskByDirName(dirName: string): Promise<TaskRecord | null> {
  const file = Bun.file(taskFile(dirName));
  if (!(await file.exists())) return null;
  const { fm, body } = parseTaskMd(await file.text());
  const implementation = await readImplementation(dirName, fm.language);
  return { ...fm, dirName, body, implementation, filePath: taskFile(dirName) };
}

type CreateTaskInput = {
  name: string;
  description?: string | null;
  mode?: string;
  language?: string | null;
};

export async function createTask(input: CreateTaskInput): Promise<TaskSummary & { filePath: string }> {
  const name = input.name.trim();
  const dirName = slugify(name);
  if (!dirName) throw new Error('Invalid name');
  if (await Bun.file(taskFile(dirName)).exists()) throw new Error('Task already exists');

  const mode = input.mode ?? 'agentic';
  const language = input.language ?? null;

  await mkdir(taskDir(dirName), { recursive: true });
  await Bun.write(
    taskFile(dirName),
    buildTaskMd({ name, description: input.description ?? null, version: 1, mode, language }, ''),
  );
  if (mode === 'script') await Bun.write(join(taskDir(dirName), implFileName(language)), '');

  return {
    dirName,
    name,
    description: input.description ?? null,
    category: null,
    mode,
    inline: false,
    version: 1,
    trigger: [],
    filePath: taskFile(dirName),
  };
}

export async function updateTask(
  dirName: string,
  patch: Partial<TaskFrontmatter> & { body?: string; implementation?: string | null },
): Promise<TaskRecord | null> {
  const existing = await getTaskByDirName(dirName);
  if (!existing) return null;

  const merged: TaskFrontmatter = { ...existing, ...patch };
  const body = patch.body ?? existing.body;
  await Bun.write(taskFile(dirName), buildTaskMd(merged, body));

  if (patch.implementation !== undefined && patch.implementation !== null) {
    await Bun.write(join(taskDir(dirName), implFileName(merged.language)), patch.implementation);
  }

  return getTaskByDirName(dirName);
}

export async function deleteTask(dirName: string): Promise<void> {
  await rm(taskDir(dirName), { recursive: true, force: true });
}

// Write a full task record to disk (used by the one-time DB→files migration). Overwrites any
// existing task with the same dirName so callers can apply their own precedence via write order.
export async function importTask(
  dirName: string,
  record: Partial<TaskFrontmatter> & { body?: string; implementation?: string | null },
): Promise<void> {
  await mkdir(taskDir(dirName), { recursive: true });
  await Bun.write(taskFile(dirName), buildTaskMd(record, record.body ?? ''));
  if (record.implementation != null && record.implementation !== '') {
    await Bun.write(join(taskDir(dirName), implFileName(record.language ?? null)), record.implementation);
  }
}
