import { promises as fs } from 'node:fs';
import path from 'node:path';

import type { Mission, MissionTask, TaskStatus } from './types.js';
import { normalizeTaskStatus } from './types.js';

const TASKS_LOCK_FILE = '.TASKS.md.lock';
const TASKS_LOCK_STALE_MS = 5 * 60 * 1000;
const TASKS_LOCK_WAIT_MS = 5 * 1000;
const TASKS_LOCK_RETRY_MS = 100;

const DEFAULT_TABLE_HEADER = [
  '| id | status | milestone | description | pr | notes |',
  '|----|--------|-----------|-------------|----|-------|',
] as const;

const DEFAULT_TASKS_PREAMBLE = [
  '# Tasks',
  '',
  '> Single-writer: orchestrator only. Workers read but never modify.',
  '',
  ...DEFAULT_TABLE_HEADER,
] as const;

interface ParsedTableRow {
  readonly lineIndex: number;
  readonly cells: string[];
}

interface ParsedTable {
  readonly headerLineIndex: number;
  readonly separatorLineIndex: number;
  readonly headers: string[];
  readonly rows: ParsedTableRow[];
  readonly idColumn: number;
  readonly statusColumn: number;
}

function normalizeHeaderName(input: string): string {
  return input.trim().toLowerCase();
}

function splitMarkdownRow(line: string): string[] {
  const trimmed = line.trim();
  if (!trimmed.startsWith('|')) {
    return [];
  }

  const parts = trimmed.split(/(?<!\\)\|/);
  if (parts.length < 3) {
    return [];
  }

  return parts.slice(1, -1).map((part) => part.trim().replace(/\\\|/g, '|'));
}

function isSeparatorRow(cells: readonly string[]): boolean {
  return (
    cells.length > 0 &&
    cells.every((cell) => {
      const value = cell.trim();
      return /^:?-{3,}:?$/.test(value);
    })
  );
}

function parseTable(content: string): ParsedTable | undefined {
  const lines = content.split(/\r?\n/);

  let headerLineIndex = -1;
  let separatorLineIndex = -1;
  let headers: string[] = [];

  for (let index = 0; index < lines.length; index += 1) {
    const cells = splitMarkdownRow(lines[index] as string);
    if (cells.length === 0) {
      continue;
    }

    const normalized = cells.map(normalizeHeaderName);
    if (!normalized.includes('id') || !normalized.includes('status')) {
      continue;
    }

    if (index + 1 >= lines.length) {
      continue;
    }

    const separatorCells = splitMarkdownRow(lines[index + 1] as string);
    if (!isSeparatorRow(separatorCells)) {
      continue;
    }

    headerLineIndex = index;
    separatorLineIndex = index + 1;
    headers = normalized;
    break;
  }

  if (headerLineIndex < 0 || separatorLineIndex < 0) {
    return undefined;
  }

  const idColumn = headers.indexOf('id');
  const statusColumn = headers.indexOf('status');
  if (idColumn < 0 || statusColumn < 0) {
    return undefined;
  }

  const rows: ParsedTableRow[] = [];
  let sawData = false;

  for (let index = separatorLineIndex + 1; index < lines.length; index += 1) {
    const rawLine = lines[index] as string;
    const trimmed = rawLine.trim();

    if (!trimmed.startsWith('|')) {
      if (sawData) {
        break;
      }
      continue;
    }

    const cells = splitMarkdownRow(rawLine);
    if (cells.length === 0) {
      if (sawData) {
        break;
      }
      continue;
    }

    sawData = true;

    const normalizedRow = [...cells];
    while (normalizedRow.length < headers.length) {
      normalizedRow.push('');
    }

    rows.push({ lineIndex: index, cells: normalizedRow });
  }

  return {
    headerLineIndex,
    separatorLineIndex,
    headers,
    rows,
    idColumn,
    statusColumn,
  };
}

function escapeTableCell(value: string): string {
  return value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
}

function formatTableRow(cells: readonly string[]): string {
  const escaped = cells.map((cell) => escapeTableCell(cell));
  return `| ${escaped.join(' | ')} |`;
}

function parseDependencies(raw: string | undefined): string[] {
  if (raw === undefined || raw.trim().length === 0) {
    return [];
  }

  return raw
    .split(',')
    .map((value) => value.trim())
    .filter((value) => value.length > 0);
}

function resolveTasksFilePath(mission: Mission): string {
  if (path.isAbsolute(mission.tasksFile)) {
    return mission.tasksFile;
  }

  return path.join(mission.projectPath, mission.tasksFile);
}

function isNodeErrorWithCode(error: unknown, code: string): boolean {
  return (
    typeof error === 'object' &&
    error !== null &&
    'code' in error &&
    (error as { code?: string }).code === code
  );
}

async function delay(ms: number): Promise<void> {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function acquireLock(lockPath: string): Promise<void> {
  const startedAt = Date.now();

  while (Date.now() - startedAt < TASKS_LOCK_WAIT_MS) {
    try {
      const handle = await fs.open(lockPath, 'wx');
      await handle.writeFile(
        JSON.stringify(
          {
            pid: process.pid,
            acquiredAt: new Date().toISOString(),
          },
          null,
          2,
        ),
      );
      await handle.close();
      return;
    } catch (error) {
      if (!isNodeErrorWithCode(error, 'EEXIST')) {
        throw error;
      }

      try {
        const stats = await fs.stat(lockPath);
        if (Date.now() - stats.mtimeMs > TASKS_LOCK_STALE_MS) {
          await fs.rm(lockPath, { force: true });
          await delay(TASKS_LOCK_RETRY_MS);
          continue;
        }
      } catch (statError) {
        if (!isNodeErrorWithCode(statError, 'ENOENT')) {
          throw statError;
        }
      }

      await delay(TASKS_LOCK_RETRY_MS);
    }
  }

  throw new Error(`Timed out acquiring TASKS lock: ${lockPath}`);
}

async function releaseLock(lockPath: string): Promise<void> {
  await fs.rm(lockPath, { force: true });
}

async function writeAtomic(filePath: string, content: string): Promise<void> {
  const directory = path.dirname(filePath);
  await fs.mkdir(directory, { recursive: true });
  const tempPath = path.join(
    directory,
    `.TASKS.md.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
  );

  await fs.writeFile(tempPath, content, 'utf8');
  await fs.rename(tempPath, filePath);
}

export function parseTasksFile(content: string): MissionTask[] {
  const parsedTable = parseTable(content);
  if (parsedTable === undefined) {
    return [];
  }

  const headerToColumn = new Map<string, number>();
  parsedTable.headers.forEach((header, index) => {
    headerToColumn.set(header, index);
  });

  const descriptionColumn = headerToColumn.get('description') ?? headerToColumn.get('title') ?? -1;
  const milestoneColumn = headerToColumn.get('milestone') ?? -1;
  const prColumn = headerToColumn.get('pr') ?? -1;
  const notesColumn = headerToColumn.get('notes') ?? -1;
  const assigneeColumn = headerToColumn.get('assignee') ?? -1;
  const dependenciesColumn = headerToColumn.get('dependencies') ?? -1;

  const tasks: MissionTask[] = [];

  for (const row of parsedTable.rows) {
    const id = row.cells[parsedTable.idColumn]?.trim();
    if (id === undefined || id.length === 0) {
      continue;
    }

    const rawStatusValue = row.cells[parsedTable.statusColumn] ?? '';
    const normalized = normalizeTaskStatus(rawStatusValue);

    const title = descriptionColumn >= 0 ? (row.cells[descriptionColumn] ?? '') : '';
    const milestone = milestoneColumn >= 0 ? (row.cells[milestoneColumn] ?? '') : '';
    const pr = prColumn >= 0 ? (row.cells[prColumn] ?? '') : '';
    const notes = notesColumn >= 0 ? (row.cells[notesColumn] ?? '') : '';
    const assignee = assigneeColumn >= 0 ? (row.cells[assigneeColumn] ?? '') : '';
    const dependenciesRaw = dependenciesColumn >= 0 ? (row.cells[dependenciesColumn] ?? '') : '';

    tasks.push({
      id,
      title,
      status: normalized.status,
      dependencies: parseDependencies(dependenciesRaw),
      milestone: milestone.length > 0 ? milestone : undefined,
      pr: pr.length > 0 ? pr : undefined,
      notes: notes.length > 0 ? notes : undefined,
      assignee: assignee.length > 0 ? assignee : undefined,
      rawStatus: normalized.rawStatus,
      line: row.lineIndex + 1,
    });
  }

  return tasks;
}

export function writeTasksFile(tasks: MissionTask[]): string {
  const lines: string[] = [...DEFAULT_TASKS_PREAMBLE];

  for (const task of tasks) {
    lines.push(
      formatTableRow([
        task.id,
        task.status,
        task.milestone ?? '',
        task.title,
        task.pr ?? '',
        task.notes ?? '',
      ]),
    );
  }

  return `${lines.join('\n')}\n`;
}

export async function updateTaskStatus(
  mission: Mission,
  taskId: string,
  status: TaskStatus,
): Promise<void> {
  const tasksFilePath = resolveTasksFilePath(mission);
  const lockPath = path.join(path.dirname(tasksFilePath), TASKS_LOCK_FILE);

  await fs.mkdir(path.dirname(tasksFilePath), { recursive: true });
  await acquireLock(lockPath);

  try {
    let content: string;
    try {
      content = await fs.readFile(tasksFilePath, 'utf8');
    } catch (error) {
      if (isNodeErrorWithCode(error, 'ENOENT')) {
        throw new Error(`TASKS file not found: ${tasksFilePath}`);
      }
      throw error;
    }

    const table = parseTable(content);
    if (table === undefined) {
      throw new Error(`Could not parse TASKS table in ${tasksFilePath}`);
    }

    const matchingRows = table.rows.filter((row) => {
      const rowTaskId = row.cells[table.idColumn]?.trim();
      return rowTaskId === taskId;
    });

    if (matchingRows.length === 0) {
      throw new Error(`Task not found in TASKS.md: ${taskId}`);
    }

    if (matchingRows.length > 1) {
      throw new Error(`Duplicate task IDs found in TASKS.md: ${taskId}`);
    }

    const targetRow = matchingRows[0] as ParsedTableRow;
    const updatedCells = [...targetRow.cells];
    updatedCells[table.statusColumn] = status;

    const lines = content.split(/\r?\n/);
    lines[targetRow.lineIndex] = formatTableRow(updatedCells);

    const updatedContent = `${lines.join('\n').replace(/\n+$/, '')}\n`;
    await writeAtomic(tasksFilePath, updatedContent);
  } finally {
    await releaseLock(lockPath);
  }
}
