import { readdir, readFile } from "node:fs/promises";
import { homedir } from "node:os";
import * as path from "node:path";
import * as process from "node:process";

import {
  ActivityAction,
  EntityType,
  Prisma,
  PrismaClient,
  ProjectStatus,
  TaskPriority,
  TaskStatus,
} from "../apps/api/node_modules/@prisma/client";

const DEFAULT_BRAIN_PATH = "~/src/jarvis-brain";
const TASKS_SUBDIR = path.join("data", "tasks");
const PROJECTS_SUBDIR = path.join("data", "projects");
const PREVIEW_LIMIT = 3;
const LOG_LIMIT = 20;

interface CliOptions {
  brainPath: string;
  workspaceId: string;
  userId: string;
  apply: boolean;
}

interface BrainTaskRecord {
  id: string;
  title: string;
  domain: string | null;
  project: string | null;
  related: string[];
  priority: string | null;
  status: string | null;
  progress: number | null;
  due: string | null;
  blocks: string[];
  blocked_by: string[];
  assignee: string | null;
  created: string | null;
  updated: string | null;
  notes: string | null;
  notes_nontechnical: string | null;
}

interface BrainTaskFile {
  version: string;
  domain: string;
  tasks: BrainTaskRecord[];
}

type BrainProjectPriority = number | string | null;

interface BrainProjectRecord {
  id: string;
  name: string;
  description: string | null;
  domain: string | null;
  status: string | null;
  priority: BrainProjectPriority;
  progress: number | null;
  repo: string | null;
  branch: string | null;
  current_milestone: string | null;
  next_milestone: string | null;
  blocker: string | null;
  owner: string | null;
  docs_path: string | null;
  created: string | null;
  updated: string | null;
  target_date: string | null;
  notes: string | null;
  notes_nontechnical: string | null;
  parent: string | null;
}

interface BrainProjectFile {
  version: string;
  project: BrainProjectRecord;
  tasks: BrainTaskRecord[];
}

interface LoadedTaskFile {
  version: string;
  domain: string;
  sourceFile: string;
  tasks: BrainTaskRecord[];
}

interface LoadedProjectFile {
  version: string;
  sourceFile: string;
  project: BrainProjectRecord;
}

interface PreparedDomain {
  slug: string;
  name: string;
  metadata: Prisma.InputJsonValue;
}

interface PreparedProject {
  brainId: string;
  name: string;
  description: string | null;
  status: ProjectStatus;
  domainSlug: string | null;
  startDate: Date | null;
  endDate: Date | null;
  createdAt: Date;
  updatedAt: Date;
  metadata: Prisma.InputJsonValue;
}

interface PreparedTask {
  brainId: string;
  title: string;
  description: string | null;
  status: TaskStatus;
  priority: TaskPriority;
  dueDate: Date | null;
  projectBrainId: string | null;
  domainSlug: string | null;
  createdAt: Date;
  updatedAt: Date;
  completedAt: Date | null;
  metadata: Prisma.InputJsonValue;
}

interface MigrationPlan {
  domains: PreparedDomain[];
  projects: PreparedProject[];
  tasks: PreparedTask[];
}

interface ValidationReport {
  taskFilesRead: number;
  projectFilesRead: number;
  tasksParsed: number;
  projectsParsed: number;
  uniqueDomainCount: number;
  duplicateTaskIds: string[];
  duplicateProjectIds: string[];
  mappingIssues: string[];
  dateIssues: string[];
  parseIssues: string[];
  missingProjectRefs: string[];
}

interface ApplySummary {
  domainsCreated: number;
  domainsSkipped: number;
  projectsCreated: number;
  projectsSkipped: number;
  tasksCreated: number;
  tasksSkipped: number;
  tasksWithMissingProject: number;
  activityLogsCreated: number;
}

function printUsage(): void {
  console.log(
    [
      "Usage:",
      "  pnpm migrate-brain --workspace-id <workspace-uuid> --user-id <user-uuid> [--brain-path <path>] [--apply]",
      "",
      "Flags:",
      "  --brain-path     Path to jarvis-brain root (default: ~/src/jarvis-brain)",
      "  --workspace-id   Target workspace UUID (required)",
      "  --user-id        Creator/user UUID for imported records (required)",
      "  --apply          Execute writes (default is dry-run)",
    ].join("\n")
  );
}

function expandHomePath(inputPath: string): string {
  if (inputPath === "~") {
    return homedir();
  }
  if (inputPath.startsWith("~/")) {
    return path.join(homedir(), inputPath.slice(2));
  }
  return inputPath;
}

function parseCliArgs(args: string[]): CliOptions {
  let brainPath = DEFAULT_BRAIN_PATH;
  let workspaceId: string | null = null;
  let userId: string | null = null;
  let apply = false;

  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];

    if (arg === "--apply") {
      apply = true;
      continue;
    }

    if (arg === "--help" || arg === "-h") {
      printUsage();
      process.exit(0);
    }

    if (arg.startsWith("--brain-path=")) {
      brainPath = arg.slice("--brain-path=".length);
      continue;
    }
    if (arg === "--brain-path") {
      const value = args[index + 1];
      if (!value) {
        throw new Error("Missing value for --brain-path");
      }
      brainPath = value;
      index += 1;
      continue;
    }

    if (arg.startsWith("--workspace-id=")) {
      workspaceId = arg.slice("--workspace-id=".length);
      continue;
    }
    if (arg === "--workspace-id") {
      const value = args[index + 1];
      if (!value) {
        throw new Error("Missing value for --workspace-id");
      }
      workspaceId = value;
      index += 1;
      continue;
    }

    if (arg.startsWith("--user-id=")) {
      userId = arg.slice("--user-id=".length);
      continue;
    }
    if (arg === "--user-id") {
      const value = args[index + 1];
      if (!value) {
        throw new Error("Missing value for --user-id");
      }
      userId = value;
      index += 1;
      continue;
    }

    throw new Error(`Unknown flag: ${arg}`);
  }

  if (!workspaceId || !userId) {
    throw new Error("Both --workspace-id and --user-id are required");
  }

  return {
    brainPath: path.resolve(expandHomePath(brainPath)),
    workspaceId,
    userId,
    apply,
  };
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function asString(value: unknown): string | null {
  return typeof value === "string" ? value : null;
}

function asNullableString(value: unknown): string | null {
  if (value === null || value === undefined) {
    return null;
  }
  return typeof value === "string" ? value : null;
}

function asNullableNumber(value: unknown): number | null {
  if (value === null || value === undefined) {
    return null;
  }
  return typeof value === "number" && Number.isFinite(value) ? value : null;
}

function asStringArray(value: unknown): string[] {
  if (!Array.isArray(value)) {
    return [];
  }

  const parsed: string[] = [];
  for (const item of value) {
    if (typeof item === "string") {
      parsed.push(item);
    }
  }
  return parsed;
}

function asStringOrNumber(value: unknown): BrainProjectPriority {
  if (value === null || value === undefined) {
    return null;
  }
  if (typeof value === "number" && Number.isFinite(value)) {
    return value;
  }
  if (typeof value === "string") {
    return value;
  }
  return null;
}

function ensureRequiredString(
  record: Record<string, unknown>,
  fieldName: string,
  context: string,
  parseIssues: string[]
): string | null {
  const value = asString(record[fieldName]);
  if (!value || value.trim().length === 0) {
    parseIssues.push(`${context}: required field "${fieldName}" missing or invalid`);
    return null;
  }
  return value;
}

function parseTaskRecord(
  rawRecord: unknown,
  context: string,
  parseIssues: string[]
): BrainTaskRecord | null {
  if (!isRecord(rawRecord)) {
    parseIssues.push(`${context}: task entry is not an object`);
    return null;
  }

  const id = ensureRequiredString(rawRecord, "id", context, parseIssues);
  const title = ensureRequiredString(rawRecord, "title", context, parseIssues);
  if (!id || !title) {
    return null;
  }

  return {
    id,
    title,
    domain: asNullableString(rawRecord.domain),
    project: asNullableString(rawRecord.project),
    related: asStringArray(rawRecord.related),
    priority: asNullableString(rawRecord.priority),
    status: asNullableString(rawRecord.status),
    progress: asNullableNumber(rawRecord.progress),
    due: asNullableString(rawRecord.due),
    blocks: asStringArray(rawRecord.blocks),
    blocked_by: asStringArray(rawRecord.blocked_by),
    assignee: asNullableString(rawRecord.assignee),
    created: asNullableString(rawRecord.created),
    updated: asNullableString(rawRecord.updated),
    notes: asNullableString(rawRecord.notes),
    notes_nontechnical: asNullableString(rawRecord.notes_nontechnical),
  };
}

function parseTaskFile(
  rawFile: unknown,
  sourceFile: string,
  parseIssues: string[]
): LoadedTaskFile | null {
  if (!isRecord(rawFile)) {
    parseIssues.push(`${sourceFile}: file payload is not an object`);
    return null;
  }

  const version = ensureRequiredString(rawFile, "version", sourceFile, parseIssues);
  const domain = ensureRequiredString(rawFile, "domain", sourceFile, parseIssues);
  const rawTasks = rawFile.tasks;

  if (!version || !domain) {
    return null;
  }
  if (!Array.isArray(rawTasks)) {
    parseIssues.push(`${sourceFile}: "tasks" is missing or not an array`);
    return null;
  }

  const tasks: BrainTaskRecord[] = [];
  for (let index = 0; index < rawTasks.length; index += 1) {
    const parsed = parseTaskRecord(rawTasks[index], `${sourceFile} task[${index}]`, parseIssues);
    if (parsed) {
      tasks.push(parsed);
    }
  }

  return {
    version,
    domain,
    sourceFile,
    tasks,
  };
}

function parseProjectRecord(
  rawRecord: unknown,
  context: string,
  parseIssues: string[]
): BrainProjectRecord | null {
  if (!isRecord(rawRecord)) {
    parseIssues.push(`${context}: "project" is not an object`);
    return null;
  }

  const id = ensureRequiredString(rawRecord, "id", context, parseIssues);
  const name = ensureRequiredString(rawRecord, "name", context, parseIssues);
  if (!id || !name) {
    return null;
  }

  return {
    id,
    name,
    description: asNullableString(rawRecord.description),
    domain: asNullableString(rawRecord.domain),
    status: asNullableString(rawRecord.status),
    priority: asStringOrNumber(rawRecord.priority),
    progress: asNullableNumber(rawRecord.progress),
    repo: asNullableString(rawRecord.repo),
    branch: asNullableString(rawRecord.branch),
    current_milestone: asNullableString(rawRecord.current_milestone),
    next_milestone: asNullableString(rawRecord.next_milestone),
    blocker: asNullableString(rawRecord.blocker),
    owner: asNullableString(rawRecord.owner),
    docs_path: asNullableString(rawRecord.docs_path),
    created: asNullableString(rawRecord.created),
    updated: asNullableString(rawRecord.updated),
    target_date: asNullableString(rawRecord.target_date),
    notes: asNullableString(rawRecord.notes),
    notes_nontechnical: asNullableString(rawRecord.notes_nontechnical),
    parent: asNullableString(rawRecord.parent),
  };
}

function parseProjectFile(
  rawFile: unknown,
  sourceFile: string,
  parseIssues: string[]
): LoadedProjectFile | null {
  if (!isRecord(rawFile)) {
    parseIssues.push(`${sourceFile}: file payload is not an object`);
    return null;
  }

  const version = ensureRequiredString(rawFile, "version", sourceFile, parseIssues);
  if (!version) {
    return null;
  }

  const project = parseProjectRecord(rawFile.project, `${sourceFile} project`, parseIssues);
  if (!project) {
    return null;
  }

  return {
    version,
    sourceFile,
    project,
  };
}

async function listJsonFiles(directoryPath: string): Promise<string[]> {
  const entries = await readdir(directoryPath, { withFileTypes: true });
  return entries
    .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
    .map((entry) => path.join(directoryPath, entry.name))
    .sort((left, right) => left.localeCompare(right));
}

async function readJsonFile(filePath: string): Promise<unknown> {
  const fileContents = await readFile(filePath, "utf8");
  return JSON.parse(fileContents) as unknown;
}

function normalizeKey(value: string | null | undefined): string {
  return value?.trim().toLowerCase() ?? "";
}

function mapTaskStatus(rawStatus: string | null): { status: TaskStatus; issue: string | null } {
  const statusKey = normalizeKey(rawStatus);
  switch (statusKey) {
    case "done":
      return { status: TaskStatus.COMPLETED, issue: null };
    case "in-progress":
      return { status: TaskStatus.IN_PROGRESS, issue: null };
    case "backlog":
    case "pending":
    case "scheduled":
    case "not-started":
    case "planned":
      return { status: TaskStatus.NOT_STARTED, issue: null };
    case "blocked":
    case "on-hold":
      return { status: TaskStatus.PAUSED, issue: null };
    case "cancelled":
      return { status: TaskStatus.ARCHIVED, issue: null };
    default:
      return {
        status: TaskStatus.NOT_STARTED,
        issue: `Unknown task status "${rawStatus ?? "null"}" mapped to NOT_STARTED`,
      };
  }
}

function mapTaskPriority(rawPriority: string | null): {
  priority: TaskPriority;
  issue: string | null;
} {
  const priorityKey = normalizeKey(rawPriority);
  switch (priorityKey) {
    case "critical":
    case "high":
      return { priority: TaskPriority.HIGH, issue: null };
    case "medium":
      return { priority: TaskPriority.MEDIUM, issue: null };
    case "low":
      return { priority: TaskPriority.LOW, issue: null };
    default:
      return {
        priority: TaskPriority.MEDIUM,
        issue: `Unknown task priority "${rawPriority ?? "null"}" mapped to MEDIUM`,
      };
  }
}

function mapProjectStatus(rawStatus: string | null): {
  status: ProjectStatus;
  issue: string | null;
} {
  const statusKey = normalizeKey(rawStatus);
  switch (statusKey) {
    case "active":
    case "in-progress":
      return { status: ProjectStatus.ACTIVE, issue: null };
    case "backlog":
    case "planning":
      return { status: ProjectStatus.PLANNING, issue: null };
    case "paused":
    case "blocked":
      return { status: ProjectStatus.PAUSED, issue: null };
    case "archived":
    case "maintenance":
      return { status: ProjectStatus.ARCHIVED, issue: null };
    default:
      return {
        status: ProjectStatus.PLANNING,
        issue: `Unknown project status "${rawStatus ?? "null"}" mapped to PLANNING`,
      };
  }
}

function normalizeDate(
  rawValue: string | null,
  context: string,
  dateIssues: string[]
): Date | null {
  if (!rawValue) {
    return null;
  }

  const trimmed = rawValue.trim();
  if (trimmed.length === 0) {
    return null;
  }

  const value = /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? `${trimmed}T00:00:00.000Z` : trimmed;
  const parsedDate = new Date(value);
  if (Number.isNaN(parsedDate.getTime())) {
    dateIssues.push(`${context}: invalid date "${rawValue}"`);
    return null;
  }

  return parsedDate;
}

function asJsonValue(value: Record<string, unknown>): Prisma.InputJsonValue {
  return value as Prisma.InputJsonValue;
}

function normalizeDomain(rawDomain: string | null | undefined): string | null {
  if (!rawDomain) {
    return null;
  }

  const trimmed = rawDomain.trim();
  if (trimmed.length === 0) {
    return null;
  }

  const slug = trimmed
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");

  return slug.length > 0 ? slug : null;
}

function truncate(items: string[], limit = LOG_LIMIT): string[] {
  return items.length <= limit ? items : items.slice(0, limit);
}

function buildMigrationPlan(
  taskFiles: LoadedTaskFile[],
  projectFiles: LoadedProjectFile[],
  parseIssues: string[],
  importTimestamp: string
): { plan: MigrationPlan; report: ValidationReport } {
  const report: ValidationReport = {
    taskFilesRead: taskFiles.length,
    projectFilesRead: projectFiles.length,
    tasksParsed: taskFiles.reduce((accumulator, file) => accumulator + file.tasks.length, 0),
    projectsParsed: projectFiles.length,
    uniqueDomainCount: 0,
    duplicateTaskIds: [],
    duplicateProjectIds: [],
    mappingIssues: [],
    dateIssues: [],
    parseIssues,
    missingProjectRefs: [],
  };

  const projectSeen = new Set<string>();
  const taskSeen = new Set<string>();
  const domainSources = new Map<string, Set<string>>();

  const registerDomain = (rawDomain: string | null | undefined): string | null => {
    const slug = normalizeDomain(rawDomain);
    if (!slug) {
      return null;
    }
    const canonical = rawDomain?.trim() ?? slug;
    const existing = domainSources.get(slug);
    if (existing) {
      existing.add(canonical);
    } else {
      domainSources.set(slug, new Set([canonical]));
    }
    return slug;
  };

  const projects: PreparedProject[] = [];
  for (const file of projectFiles) {
    const brainProject = file.project;
    if (projectSeen.has(brainProject.id)) {
      report.duplicateProjectIds.push(
        `${brainProject.id} (duplicate in ${file.sourceFile}; keeping first instance)`
      );
      continue;
    }
    projectSeen.add(brainProject.id);

    const mappedStatus = mapProjectStatus(brainProject.status);
    if (mappedStatus.issue) {
      report.mappingIssues.push(`project ${brainProject.id}: ${mappedStatus.issue}`);
    }

    const domainSlug = registerDomain(brainProject.domain);
    const createdAt =
      normalizeDate(
        brainProject.created,
        `project ${brainProject.id}.created`,
        report.dateIssues
      ) ?? new Date();
    const updatedAt =
      normalizeDate(
        brainProject.updated,
        `project ${brainProject.id}.updated`,
        report.dateIssues
      ) ?? createdAt;
    const startDate =
      normalizeDate(
        brainProject.created,
        `project ${brainProject.id}.startDate`,
        report.dateIssues
      ) ?? null;
    const endDate = normalizeDate(
      brainProject.target_date,
      `project ${brainProject.id}.target_date`,
      report.dateIssues
    );

    const metadata = asJsonValue({
      source: "jarvis-brain",
      brainId: brainProject.id,
      brainDomain: brainProject.domain,
      rawStatus: brainProject.status,
      rawPriority: brainProject.priority,
      progress: brainProject.progress,
      repo: brainProject.repo,
      branch: brainProject.branch,
      currentMilestone: brainProject.current_milestone,
      nextMilestone: brainProject.next_milestone,
      blocker: brainProject.blocker,
      owner: brainProject.owner,
      docsPath: brainProject.docs_path,
      targetDate: brainProject.target_date,
      notes: brainProject.notes,
      notesNonTechnical: brainProject.notes_nontechnical,
      parent: brainProject.parent,
      sourceFile: file.sourceFile,
      sourceVersion: file.version,
      importedAt: importTimestamp,
    });

    projects.push({
      brainId: brainProject.id,
      name: brainProject.name,
      description: brainProject.description,
      status: mappedStatus.status,
      domainSlug,
      startDate,
      endDate,
      createdAt,
      updatedAt,
      metadata,
    });
  }

  const tasks: PreparedTask[] = [];
  for (const file of taskFiles) {
    registerDomain(file.domain);

    for (const brainTask of file.tasks) {
      if (taskSeen.has(brainTask.id)) {
        report.duplicateTaskIds.push(
          `${brainTask.id} (duplicate in ${file.sourceFile}; keeping first instance)`
        );
        continue;
      }
      taskSeen.add(brainTask.id);

      const mappedStatus = mapTaskStatus(brainTask.status);
      if (mappedStatus.issue) {
        report.mappingIssues.push(`task ${brainTask.id}: ${mappedStatus.issue}`);
      }

      const mappedPriority = mapTaskPriority(brainTask.priority);
      if (mappedPriority.issue) {
        report.mappingIssues.push(`task ${brainTask.id}: ${mappedPriority.issue}`);
      }

      const domainSlug = registerDomain(brainTask.domain ?? file.domain);
      const projectBrainId = brainTask.project?.trim() ? brainTask.project.trim() : null;
      if (projectBrainId && !projectSeen.has(projectBrainId)) {
        report.missingProjectRefs.push(
          `task ${brainTask.id}: referenced project "${projectBrainId}" not found in project files`
        );
      }

      const createdAt =
        normalizeDate(brainTask.created, `task ${brainTask.id}.created`, report.dateIssues) ??
        new Date();
      const updatedAt =
        normalizeDate(brainTask.updated, `task ${brainTask.id}.updated`, report.dateIssues) ??
        createdAt;
      const dueDate = normalizeDate(brainTask.due, `task ${brainTask.id}.due`, report.dateIssues);
      const completedAt = mappedStatus.status === TaskStatus.COMPLETED ? updatedAt : null;

      const metadata = asJsonValue({
        source: "jarvis-brain",
        brainId: brainTask.id,
        brainDomain: brainTask.domain ?? file.domain,
        brainProjectId: projectBrainId,
        rawStatus: brainTask.status,
        rawPriority: brainTask.priority,
        related: brainTask.related,
        blocks: brainTask.blocks,
        blockedBy: brainTask.blocked_by,
        assignee: brainTask.assignee,
        progress: brainTask.progress,
        notes: brainTask.notes,
        notesNonTechnical: brainTask.notes_nontechnical,
        sourceFile: file.sourceFile,
        sourceVersion: file.version,
        importedAt: importTimestamp,
      });

      tasks.push({
        brainId: brainTask.id,
        title: brainTask.title,
        description: brainTask.notes,
        status: mappedStatus.status,
        priority: mappedPriority.priority,
        dueDate,
        projectBrainId,
        domainSlug,
        createdAt,
        updatedAt,
        completedAt,
        metadata,
      });
    }
  }

  const domains: PreparedDomain[] = [];
  for (const [slug, rawValues] of Array.from(domainSources.entries())) {
    const sourceValues = [...rawValues].sort((left, right) => left.localeCompare(right));
    const domainName = sourceValues[0] ?? slug;
    domains.push({
      slug,
      name: domainName,
      metadata: asJsonValue({
        source: "jarvis-brain",
        brainId: domainName,
        sourceValues,
        importedAt: importTimestamp,
      }),
    });
  }

  domains.sort((left, right) => left.slug.localeCompare(right.slug));
  report.uniqueDomainCount = domains.length;

  return {
    plan: {
      domains,
      projects,
      tasks,
    },
    report,
  };
}

function printValidationReport(
  options: CliOptions,
  plan: MigrationPlan,
  report: ValidationReport
): void {
  console.log("");
  console.log("=== jarvis-brain migration validation ===");
  console.log(`Mode: ${options.apply ? "APPLY" : "DRY-RUN"}`);
  console.log(`Brain path: ${options.brainPath}`);
  console.log(`Workspace ID: ${options.workspaceId}`);
  console.log(`User ID: ${options.userId}`);
  console.log("");
  console.log("Counts:");
  console.log(`- Task files read: ${report.taskFilesRead}`);
  console.log(`- Project files read: ${report.projectFilesRead}`);
  console.log(`- Parsed tasks: ${report.tasksParsed}`);
  console.log(`- Parsed projects: ${report.projectsParsed}`);
  console.log(`- Unique domains: ${report.uniqueDomainCount}`);
  console.log("");
  console.log("Planned inserts:");
  console.log(`- Domains: ${plan.domains.length}`);
  console.log(`- Projects: ${plan.projects.length}`);
  console.log(`- Tasks: ${plan.tasks.length}`);

  console.log("");
  console.log("Validation issues:");
  console.log(`- Parse issues: ${report.parseIssues.length}`);
  console.log(`- Date issues: ${report.dateIssues.length}`);
  console.log(`- Mapping issues: ${report.mappingIssues.length}`);
  console.log(`- Missing project references: ${report.missingProjectRefs.length}`);
  console.log(`- Duplicate project IDs: ${report.duplicateProjectIds.length}`);
  console.log(`- Duplicate task IDs: ${report.duplicateTaskIds.length}`);

  const printIssueSection = (label: string, values: string[]): void => {
    if (values.length === 0) {
      return;
    }
    console.log("");
    console.log(`${label} (showing up to ${LOG_LIMIT}):`);
    for (const issue of truncate(values)) {
      console.log(`- ${issue}`);
    }
  };

  printIssueSection("Parse issues", report.parseIssues);
  printIssueSection("Date issues", report.dateIssues);
  printIssueSection("Mapping issues", report.mappingIssues);
  printIssueSection("Missing project references", report.missingProjectRefs);
  printIssueSection("Duplicate project IDs", report.duplicateProjectIds);
  printIssueSection("Duplicate task IDs", report.duplicateTaskIds);

  console.log("");
  console.log(`Insert payload preview (first ${PREVIEW_LIMIT} each):`);
  console.log(
    JSON.stringify(
      {
        domains: plan.domains.slice(0, PREVIEW_LIMIT),
        projects: plan.projects.slice(0, PREVIEW_LIMIT),
        tasks: plan.tasks.slice(0, PREVIEW_LIMIT),
      },
      null,
      2
    )
  );
}

async function ensureWorkspaceAndUserExist(
  prisma: PrismaClient,
  workspaceId: string,
  userId: string
): Promise<void> {
  const [workspace, user] = await Promise.all([
    prisma.workspace.findUnique({
      where: { id: workspaceId },
      select: { id: true },
    }),
    prisma.user.findUnique({
      where: { id: userId },
      select: { id: true },
    }),
  ]);

  if (!workspace) {
    throw new Error(`Workspace not found for --workspace-id=${workspaceId}`);
  }
  if (!user) {
    throw new Error(`User not found for --user-id=${userId}`);
  }
}

function buildCreatedActivityDetails(
  source: "domain" | "project" | "task",
  brainId: string,
  importTimestamp: string
): Prisma.InputJsonValue {
  return asJsonValue({
    source: "jarvis-brain",
    migrationEntity: source,
    brainId,
    importedAt: importTimestamp,
  });
}

async function applyMigration(
  prisma: PrismaClient,
  options: CliOptions,
  plan: MigrationPlan,
  importTimestamp: string
): Promise<ApplySummary> {
  await ensureWorkspaceAndUserExist(prisma, options.workspaceId, options.userId);

  return prisma.$transaction(async (tx) => {
    const domainIdBySlug = new Map<string, string>();
    const projectIdByBrainId = new Map<string, string>();

    let domainsCreated = 0;
    let domainsSkipped = 0;
    let projectsCreated = 0;
    let projectsSkipped = 0;
    let tasksCreated = 0;
    let tasksSkipped = 0;
    let tasksWithMissingProject = 0;
    let activityLogsCreated = 0;

    for (const domain of plan.domains) {
      const existingDomain = await tx.domain.findUnique({
        where: {
          workspaceId_slug: {
            workspaceId: options.workspaceId,
            slug: domain.slug,
          },
        },
        select: { id: true },
      });

      if (existingDomain) {
        domainIdBySlug.set(domain.slug, existingDomain.id);
        domainsSkipped += 1;
        continue;
      }

      const createdDomain = await tx.domain.create({
        data: {
          workspaceId: options.workspaceId,
          name: domain.name,
          slug: domain.slug,
          metadata: domain.metadata,
        },
        select: { id: true },
      });

      domainIdBySlug.set(domain.slug, createdDomain.id);
      domainsCreated += 1;

      await tx.activityLog.create({
        data: {
          workspaceId: options.workspaceId,
          userId: options.userId,
          action: ActivityAction.CREATED,
          entityType: EntityType.DOMAIN,
          entityId: createdDomain.id,
          details: buildCreatedActivityDetails("domain", domain.name, importTimestamp),
        },
      });
      activityLogsCreated += 1;
    }

    for (const project of plan.projects) {
      const existingProject = await tx.project.findFirst({
        where: {
          workspaceId: options.workspaceId,
          metadata: {
            path: ["brainId"],
            equals: project.brainId,
          },
        },
        select: { id: true },
      });

      if (existingProject) {
        projectIdByBrainId.set(project.brainId, existingProject.id);
        projectsSkipped += 1;
        continue;
      }

      const createdProject = await tx.project.create({
        data: {
          workspaceId: options.workspaceId,
          name: project.name,
          description: project.description,
          status: project.status,
          startDate: project.startDate,
          endDate: project.endDate,
          creatorId: options.userId,
          domainId: project.domainSlug ? (domainIdBySlug.get(project.domainSlug) ?? null) : null,
          metadata: project.metadata,
          createdAt: project.createdAt,
          updatedAt: project.updatedAt,
        },
        select: { id: true },
      });

      projectIdByBrainId.set(project.brainId, createdProject.id);
      projectsCreated += 1;

      await tx.activityLog.create({
        data: {
          workspaceId: options.workspaceId,
          userId: options.userId,
          action: ActivityAction.CREATED,
          entityType: EntityType.PROJECT,
          entityId: createdProject.id,
          details: buildCreatedActivityDetails("project", project.brainId, importTimestamp),
        },
      });
      activityLogsCreated += 1;
    }

    for (const task of plan.tasks) {
      const existingTask = await tx.task.findFirst({
        where: {
          workspaceId: options.workspaceId,
          metadata: {
            path: ["brainId"],
            equals: task.brainId,
          },
        },
        select: { id: true },
      });

      if (existingTask) {
        tasksSkipped += 1;
        continue;
      }

      const projectId =
        task.projectBrainId !== null ? (projectIdByBrainId.get(task.projectBrainId) ?? null) : null;
      if (task.projectBrainId && !projectId) {
        tasksWithMissingProject += 1;
      }

      const createdTask = await tx.task.create({
        data: {
          workspaceId: options.workspaceId,
          title: task.title,
          description: task.description,
          status: task.status,
          priority: task.priority,
          dueDate: task.dueDate,
          creatorId: options.userId,
          projectId,
          domainId: task.domainSlug ? (domainIdBySlug.get(task.domainSlug) ?? null) : null,
          metadata: task.metadata,
          createdAt: task.createdAt,
          updatedAt: task.updatedAt,
          completedAt: task.completedAt,
        },
        select: { id: true },
      });

      tasksCreated += 1;

      await tx.activityLog.create({
        data: {
          workspaceId: options.workspaceId,
          userId: options.userId,
          action: ActivityAction.CREATED,
          entityType: EntityType.TASK,
          entityId: createdTask.id,
          details: buildCreatedActivityDetails("task", task.brainId, importTimestamp),
        },
      });
      activityLogsCreated += 1;
    }

    return {
      domainsCreated,
      domainsSkipped,
      projectsCreated,
      projectsSkipped,
      tasksCreated,
      tasksSkipped,
      tasksWithMissingProject,
      activityLogsCreated,
    };
  });
}

async function loadTaskFiles(
  taskDirectory: string,
  parseIssues: string[],
  rootPath: string
): Promise<LoadedTaskFile[]> {
  const files = await listJsonFiles(taskDirectory);
  const loadedFiles: LoadedTaskFile[] = [];

  for (const filePath of files) {
    const payload = await readJsonFile(filePath);
    const sourceFile = path.relative(rootPath, filePath);
    const parsed = parseTaskFile(payload, sourceFile, parseIssues);
    if (parsed) {
      loadedFiles.push(parsed);
    }
  }

  return loadedFiles;
}

async function loadProjectFiles(
  projectDirectory: string,
  parseIssues: string[],
  rootPath: string
): Promise<LoadedProjectFile[]> {
  const files = await listJsonFiles(projectDirectory);
  const loadedFiles: LoadedProjectFile[] = [];

  for (const filePath of files) {
    const payload = await readJsonFile(filePath);
    const sourceFile = path.relative(rootPath, filePath);
    const parsed = parseProjectFile(payload, sourceFile, parseIssues);
    if (parsed) {
      loadedFiles.push(parsed);
    }
  }

  return loadedFiles;
}

async function main(): Promise<void> {
  const options = parseCliArgs(process.argv.slice(2));
  const importTimestamp = new Date().toISOString();

  const tasksPath = path.join(options.brainPath, TASKS_SUBDIR);
  const projectsPath = path.join(options.brainPath, PROJECTS_SUBDIR);

  const parseIssues: string[] = [];
  const [taskFiles, projectFiles] = await Promise.all([
    loadTaskFiles(tasksPath, parseIssues, options.brainPath),
    loadProjectFiles(projectsPath, parseIssues, options.brainPath),
  ]);

  const { plan, report } = buildMigrationPlan(
    taskFiles,
    projectFiles,
    parseIssues,
    importTimestamp
  );
  printValidationReport(options, plan, report);

  if (!options.apply) {
    console.log("");
    console.log("Dry-run complete. Re-run with --apply to write records.");
    return;
  }

  const prisma = new PrismaClient();
  try {
    const summary = await applyMigration(prisma, options, plan, importTimestamp);
    console.log("");
    console.log("Apply complete:");
    console.log(`- Domains created/skipped: ${summary.domainsCreated}/${summary.domainsSkipped}`);
    console.log(
      `- Projects created/skipped: ${summary.projectsCreated}/${summary.projectsSkipped}`
    );
    console.log(`- Tasks created/skipped: ${summary.tasksCreated}/${summary.tasksSkipped}`);
    console.log(`- Tasks with missing project links: ${summary.tasksWithMissingProject}`);
    console.log(`- Activity logs created: ${summary.activityLogsCreated}`);
  } finally {
    await prisma.$disconnect();
  }
}

main().catch((error: unknown) => {
  const message = error instanceof Error ? error.message : String(error);
  console.error(`Migration failed: ${message}`);
  printUsage();
  process.exitCode = 1;
});
