import { readdir, readFile, stat } from "node:fs/promises";
import { join, resolve, relative, dirname, basename } from "node:path";
import chalk from "chalk";
import { type CommandResult, success, silentFailure } from "../../lib/command-result";

interface ValidationContext {
  failures: number;
}

function pass(msg: string) {
  console.log(chalk.green(`\u2713 ${msg}`));
}

function fail(ctx: ValidationContext, msg: string) {
  console.log(chalk.red(`\u2717 ${msg}`));
  ctx.failures++;
}

async function subdirs(dir: string): Promise<string[]> {
  const entries = await readdir(dir);
  const dirs: string[] = [];
  for (const entry of entries) {
    const full = join(dir, entry);
    const s = await stat(full);
    if (s.isDirectory()) dirs.push(entry);
  }
  return dirs.sort();
}

function checkStructure(ctx: ValidationContext, entries: string[], label: string): boolean {
  if (entries.includes("README.md")) {
    pass(`${label}: has README.md`);
  } else {
    fail(ctx, `${label}: missing README.md`);
  }

  if (entries.includes("mock.json")) {
    pass(`${label}: has mock.json`);
    return true;
  }
  fail(ctx, `${label}: missing mock.json`);
  return false;
}

async function checkSchema(
  ctx: ValidationContext,
  data: MockoonData,
  label: string,
): Promise<boolean> {
  const commons = await import("@mockoon/commons");
  // EnvironmentSchemaNoFix is a Joi schema typed as `any` in @mockoon/commons
  const schema = commons.EnvironmentSchemaNoFix as {
    validate: (
      value: unknown,
      options?: { abortEarly?: boolean },
    ) => { error?: { details: { message: string }[] } };
  };
  const result = schema.validate(data, { abortEarly: false });
  if (!result.error) {
    pass(`${label}: valid Mockoon schema`);
    return true;
  }
  for (const detail of result.error.details) {
    fail(ctx, `${label}: schema — ${detail.message}`);
  }
  return false;
}

interface MockoonRoute {
  uuid: string;
  responses?: MockoonResponse[];
}

interface MockoonResponse {
  uuid: string;
  label?: string;
  headers?: { key: string; value: string }[];
  bodyType?: string;
  databucketID?: string;
}

interface MockoonData {
  uuid?: string;
  name?: string;
  port?: number;
  routes?: MockoonRoute[];
  data?: { id: string }[];
}

function checkResponseQuality(ctx: ValidationContext, data: MockoonData, label: string) {
  const routes = data.routes ?? [];
  for (const route of routes) {
    for (const resp of route.responses ?? []) {
      const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
      const headers = resp.headers ?? [];
      const hasContentType = headers.some((h) => h.key.toLowerCase() === "content-type");
      if (hasContentType) {
        pass(`${respLabel}: has Content-Type`);
      } else {
        fail(ctx, `${respLabel}: missing Content-Type header`);
      }
      if (resp.label && resp.label.trim().length > 0) {
        pass(`${respLabel}: has label "${resp.label}"`);
      } else {
        fail(ctx, `${respLabel}: missing or empty label`);
      }
    }
  }
}

function checkDatabucketRefs(ctx: ValidationContext, data: MockoonData, label: string) {
  const bucketIds = new Set((data.data ?? []).map((d) => d.id));
  for (const route of data.routes ?? []) {
    for (const resp of route.responses ?? []) {
      if (resp.bodyType === "DATABUCKET") {
        const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
        if (resp.databucketID && bucketIds.has(resp.databucketID)) {
          pass(`${respLabel}: databucket "${resp.databucketID}" exists`);
        } else {
          fail(ctx, `${respLabel}: databucketID "${resp.databucketID}" not found in data array`);
        }
      }
    }
  }
}

async function discoverAllScenarios(mocksDir: string): Promise<string[]> {
  const scenarios: string[] = [];
  const providers = await subdirs(mocksDir);
  for (const provider of providers) {
    const providerDir = join(mocksDir, provider);
    for (const scenario of await subdirs(providerDir)) {
      scenarios.push(`${provider}/${scenario}`);
    }
  }
  return scenarios;
}

function resolveScenarioDir(arg: string): string {
  const abs = resolve(arg);
  if (basename(abs) === "mock.json") return dirname(abs);
  return abs;
}

async function validateScenario(ctx: ValidationContext, scenarioDir: string, mocksDir: string) {
  const label = relative(mocksDir, scenarioDir);
  console.log(chalk.bold(`\n\u2500\u2500 ${label} \u2500\u2500`));

  let entries: string[];
  try {
    entries = await readdir(scenarioDir);
  } catch {
    fail(ctx, `${label}: directory not found`);
    return;
  }

  const hasMock = checkStructure(ctx, entries, label);
  if (!hasMock) return;

  const mockPath = join(scenarioDir, "mock.json");

  let data: MockoonData;
  try {
    data = JSON.parse(await readFile(mockPath, "utf-8")) as MockoonData;
  } catch (err: unknown) {
    const errMsg = err instanceof Error ? err.message : String(err);
    fail(ctx, `${label}: invalid JSON \u2014 ${errMsg}`);
    return;
  }

  // The quality checks below can throw on schema-invalid data
  const schemaOk = await checkSchema(ctx, data, label);
  if (!schemaOk) return;
  checkResponseQuality(ctx, data, label);
  checkDatabucketRefs(ctx, data, label);
}

export async function runMockValidate(mocksRoot: string, paths: string[]): Promise<CommandResult> {
  const ctx: ValidationContext = { failures: 0 };
  const mocksDir = resolve(mocksRoot);

  const targets =
    paths.length > 0
      ? paths.map(resolveScenarioDir)
      : (await discoverAllScenarios(mocksDir)).map((s) => join(mocksDir, s));

  if (targets.length === 0) {
    fail(ctx, "No scenarios found under mocks/");
    return silentFailure();
  }

  console.log(chalk.bold("\nValidating mock configs...\n"));

  for (const target of targets) {
    await validateScenario(ctx, target, mocksDir);
  }

  console.log(chalk.bold("\n\u2500\u2500 summary \u2500\u2500"));
  if (ctx.failures === 0) {
    console.log(chalk.green("\u2713 All checks passed"));
  } else {
    console.log(chalk.red(`\u2717 ${ctx.failures} check(s) failed`));
  }

  return ctx.failures === 0 ? success() : silentFailure();
}
