import { MigrateTestOptionsModel } from "../model/studio/command-options/migrate-test-options-model.js";
import { DebugManager } from "../debug/debug-manager.js";
import fs from "fs";
import path from "path";
import { parse } from "yaml";
import {
  AtmConfig,
  AtmInfo,
  AtmTestConfig,
  TestStep,
  TestStepType,
} from "../model/atm-test-model.js";
import { Assert } from "@apic/api-model/test/common/Assert.js";
import { Test, Test_Request, Test_Payload } from "@apic/api-model/test/Test.js";
import yaml from "js-yaml";
import { showError, showInfo } from "../helpers/common/message-helper.js";
import {
  MIGRATION_STARTED,
  MIGRATION_COMPLETED,
} from "../constants/message-constants.js";
import { ASSERTION, ENVIRONMENT, TEST } from "../constants/app-constants.js";
import { Environment } from "@apic/api-model/test/Environment.js";
import { Assertion } from "@apic/api-model/test/Assertion.js";

const namespace = "default";
const apiVersion = "api.webmethods.io/beta";
const version = "1.0.0";

export const setupDebugManager = (debug: boolean): DebugManager => {
  const debugManager = DebugManager.getInstance();
  debugManager.setDebugEnabled(debug);
  return debugManager;
};

const isYamlFile = (path: string) => {
  return path.endsWith(".yml") || path.endsWith(".yaml");
};

const readYamlFile = (filePath: string): AtmTestConfig | null => {
  try {
    const content = fs.readFileSync(filePath, "utf8");
    const test = parse(content) as AtmTestConfig;
    if (!Array.isArray(test.steps) || test.steps.length === 0) {
      throw new Error("Missing or empty 'steps' in test");
    }
    return test;
  } catch (error) {
    showError(`Failed to read YAML file: ${filePath}`);
    return null;
  }
};

const readYamlFromPath = (
  inputPath: string
): Array<{ fileName: string; test: AtmTestConfig }> => {
  const stat = fs.statSync(inputPath);
  let result: Array<{ fileName: string; test: AtmTestConfig }> = [];
  if (stat.isDirectory()) {
    const files = fs.readdirSync(inputPath);
    for (const file of files) {
      if (isYamlFile(file)) {
        const fullPath = path.join(inputPath, file);
        const test = readYamlFile(fullPath);
        const fileName = path.basename(fullPath, path.extname(fullPath));
        if (test) result.push({ fileName, test });
      }
    }
  } else if (stat.isFile() && isYamlFile(inputPath)) {
    const fileName = path.basename(inputPath, path.extname(inputPath));
    const test = readYamlFile(inputPath);
    if (test) result.push({ fileName, test });
  } else {
    showError(`Unsupported path type: ${inputPath}`);
  }

  return result;
};

const mapToKeyValue = (
  headers?: Record<string, string> | undefined
): Array<{ key: string; value: string }> | undefined => {
  if (!headers || typeof headers !== "object") return undefined;
  return Object.entries(headers).map(([key, value]) => ({
    key,
    value: mapVariables(value) ?? "",
  }));
};

const mapBodyToPayload = (mode?: string, body?: any): Test_Payload => {
  const payload: Test_Payload = {};
  if (!mode || body == null) return payload;
  payload.raw = {
    ['json']: mode === "json" ? `${JSON.stringify(body)}` : String(body),
  };
  return payload;
};

// Map .[0] → .0
const normalizeArrayFormat = (str?: string): string | undefined => {
  if (!str || typeof str !== "string") return str;
  return str.replace(/\.\[(\d+)\]/g, ".$1");
};

// Map {{ variable }} to ${variable} for studio
const mapVariables = (str?: string): string | undefined => {
  if (!str || typeof str !== "string") return str;

  return str.replace(
    /{{\s*(.*?)\s*}}/g,
    (_, expression) => `\${${normalizeArrayFormat(expression)}}`
  );
};

const mapExpressions = (
  input?: string | number,
  variableName?: string
): string | undefined => {
  const str = input?.toString();
  if (!str) return str;

  switch (true) {
    case str.endsWith("_response_statusCode"):
      return "${code}";
    case str.includes("_response_header_"):
      return str
        .replace(`${variableName}_response_header_`, "headers().")
        .toLocaleLowerCase();
    default: {
      if (!variableName) return str;
      return `\${${normalizeArrayFormat(str)}}`;
    }
  }
};

const mapAssertions = (
  step: TestStep,
  ifExpression?: string,
  variableName?: string
): Assert | undefined => {
  const {
    type,
    stoponfail: stopOnFail,
    value: rawValue,
    values: rawValues,
    expression: rawExpression,
    expression1: rawExpression1,
    expression2: rawExpression2,
  } = step;

  const expression = mapExpressions(rawExpression, variableName);
  const expression1 = mapExpressions(rawExpression1, variableName);
  const expression2 = mapExpressions(rawExpression2, variableName);
  const value = mapVariables(rawValue);
  const values = rawValues?.map(mapVariables);
  const ifExpressionVariable = mapExpressions(ifExpression, variableName);

  const base = {
    ...(stopOnFail?.toString() === "true" ? { stopOnFail: true } : {}),
    ...(ifExpressionVariable ? { if: ifExpressionVariable } : {}),
  };

  switch (type) {
    case TestStepType.AssertEquals:
    case TestStepType.AssertCompares:
      return {
        ...base,
        action: "equals",
        name: `${type} => equals`,
        key: type === TestStepType.AssertCompares ? expression1 : expression,
        value: type === TestStepType.AssertCompares ? expression2 : value,
      } as Assert;
    case TestStepType.AssertGreater:
      return {
        ...base,
        action: "greaterThan",
        name: `${type} => greaterThan`,
        key: expression,
        value,
      } as Assert;
    case TestStepType.AssertLess:
      return {
        ...base,
        action: "lessThan",
        name: `${type} => lessThan`,
        key: expression,
        value,
      } as Assert;
    case TestStepType.AssertIs:
      return {
        ...base,
        action: "type",
        name: `${type} => type`,
        key: expression,
        value,
      } as Assert;
    case TestStepType.AssertContains:
      return {
        ...base,
        action: "include",
        name: `${type} => include`,
        key: expression,
        value: value,
      } as Assert;
    case TestStepType.AssertExists:
      return {
        ...base,
        action: "haveProperty",
        name: `${type} => haveProperty`,
        key: expression,
        value,
      } as Assert;
    case TestStepType.AssertIn:
      return {
        ...base,
        action: "include",
        name: `${type} => include`,
        key: values,
        value: expression,
      } as Assert;
    case TestStepType.AssertMatches:
      return {
        ...base,
        action: "matches",
        name: `${type} => matches`,
        key: expression,
        value,
      } as Assert;
    default:
      return;
  }
};

const splitUrlIntoBaseAndPath = (url?: string): Array<string> => {
  if (!url) return ["", ""];
  const lastSlashIndex = url?.lastIndexOf("/");
  if (lastSlashIndex === -1 || lastSlashIndex === url.length - 1) {
    // No slash found or trailing slash with nothing after

    return [url, ""];
  }
  return [url.substring(lastSlashIndex), url.substring(0, lastSlashIndex)];
};

const getRequests = (
  outputDir: string,
  fileName: string,
  steps: Array<TestStep>
): Array<Test_Request> => {
  if (!steps?.length) return [];
  const requests: Array<Test_Request> = [];
  let currentRequest: Test_Request | undefined;

  for (const step of steps) {
    const {
      type,
      method,
      var: variableName,
      body,
      mode,
      headers,
      url,
      params,
      steps: subSteps,
      expression,
    } = step;

    if (type === TestStepType.Request) {
      const [resource, endpoint] = splitUrlIntoBaseAndPath(mapVariables(url));

      currentRequest = {
        method,
        resource,
        var: variableName,
        ...(body && mode ? { payload: mapBodyToPayload(mode, body) } : {}),
        ...(headers ? { headers: mapToKeyValue(headers) } : {}),
        ...(params ? { parameters: mapToKeyValue(params) } : {}),
        assertions: { expressions: [] },
        endpoint,
      } as Test_Request;
      requests.push(currentRequest);
    } else {
      if (!currentRequest) {
        showError(`Assertion step without a preceding request`);
        continue;
      }
      const targets = type === TestStepType.If ? subSteps ?? [] : [step];
      for (const sub of targets) {
        const assertion = mapAssertions(
          sub,
          type === TestStepType.If ? expression : undefined,
          currentRequest.var
        );
        if (assertion) currentRequest.assertions?.expressions?.push(assertion);
      }
    }
  }

  // Iterate requests and replace assertion with Ref
  for (let i = 0; i < requests.length; i++) {
    const request = requests[i];
    const assertionRef = getAssertionRef(
      outputDir,
      fileName,
      i,
      request.assertions?.expressions
    );
    request.assertions = {
      $ref: assertionRef,
    };
  }

  return requests;
};

const getAssertionRef = (
  outputDir: string,
  fileName: string,
  index: number,
  spec?: Array<Assert>
): string => {
  const suffix = index > 0 ? `_${index}` : "";
  const name = `assertion${suffix}_${formatTitle(fileName)}`;
  let assertionRef = getRef(namespace, name, version);
  const assertion: Assertion = {
    kind: ASSERTION,
    apiVersion,
    metadata: generateMetadata(name),
    spec,
  };
  try {
    writeFile(assertion, outputDir, name);
  } catch {
    showError(`Failed to write assertion for ${fileName}`);
  }
  return assertionRef;
};

const formatTitle = (title?: string): string =>
  title?.toLowerCase()?.replace(/\s/g, "") ?? "untitled";

const getRef = (namespace: string, name: string, version: string) =>
  `${namespace}:${name}:${version}`;

const generateMetadata = (name: string) => ({
  name,
  namespace,
  version,
  tags: [],
});

const getEnvironmentRefs = (
  outputDir: string,
  fileName: string,
  configs: AtmConfig,
  info: AtmInfo
): Array<string> => {
  const { version: atmVersion } = info;
  const environmentRefs: Array<string> = [];
  const environments: Record<
    string,
    Array<{ key: string; value: string }>
  > = {};

  if (atmVersion?.toString() == "2") {
    const { globalVariables, inputs } = configs;
    for (const input of inputs) {
      const [label, inputVariables] = Object.entries(input)[0];
      environments[label] = [
        ...(mapToKeyValue(inputVariables) ?? []),
        ...(mapToKeyValue(globalVariables) ?? []),
      ];
    }
  } else {
    environments["default"] = [...(mapToKeyValue(configs) ?? [])];
  }

  for (const [label, variables] of Object.entries(environments)) {
    const name = `environment_${formatTitle(label)}_${formatTitle(fileName)}`;
    const environment: Environment = {
      kind: ENVIRONMENT,
      apiVersion,
      metadata: generateMetadata(name),
      spec: {
        variables: variables.map((variable) => ({
          ...variable,
          isSecret: false,
        })),
      },
    };
    try {
      writeFile(environment, outputDir, name);
      environmentRefs.push(getRef(namespace, name, version));
    } catch {
      showError(
        `Failed to write ${label} variables as environment for ${fileName}`
      );
    }
  }

  return environmentRefs;
};

const generateTest = (
  { info, steps, configs }: AtmTestConfig,
  outputDir: string,
  fileName: string,
  debugManager: DebugManager
): void => {
  const environmentRefs = getEnvironmentRefs(
    outputDir,
    fileName,
    configs,
    info ?? {}
  );

  if (debugManager.isDebugEnabled()) {
    showInfo(
      `Generated following EnvironmentRefs: ${environmentRefs} for ${fileName}`
    );
  }

  const request = getRequests(outputDir, fileName, steps ?? []);

  if (debugManager.isDebugEnabled()) {
    showInfo(`Generated ${request.length} for ${fileName}`);
  }

  const test: Test = {
    kind: TEST,
    apiVersion,
    metadata: generateMetadata(`Test for ${formatTitle(fileName)}`),
    spec: {
      api: {
        $endpoint: "// Replace with a valid endpoint",
      },
      environment: {
        $ref: environmentRefs[0], //map only one environment for current support
      },
      request,
    },
  };
  if (debugManager.isDebugEnabled()) {
    showInfo(`Generated test ${request.length} for ${fileName}`);
  }

  try {
    writeFile(test, outputDir, fileName);
  } catch {
    showError(`Failed to write assertion for ${fileName}`);
  }
};

const uniqueFilePath = (outputDir: string, fileName: string): string => {
  const ext = ".yaml";
  let candidate = path.join(outputDir, `${fileName}${ext}`);
  let count = 1;

  // Keep incrementing the suffix until we find a file name that doesn't exist
  while (fs.existsSync(candidate)) {
    candidate = path.join(outputDir, `${fileName}_${count}${ext}`);
    count++;
  }

  return candidate;
};

const writeFile = (
  file: Test | Environment | Assertion,
  output: string,
  fileName: string
) => {
  const outputDir = path.resolve(output);
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }
  const filePath = uniqueFilePath(outputDir, fileName);
  const yamlString = yaml.dump(file, { noRefs: true });
  fs.writeFileSync(filePath, yamlString, "utf8");
};

const migrateTestAction = async (options: MigrateTestOptionsModel) => {
  const { localDir, output, debug } = options;
  const debugManager = setupDebugManager(Boolean(debug));
  showInfo(MIGRATION_STARTED);
  try {
    if (debugManager.isDebugEnabled())
      showInfo(`Started reading ATM test files from: ${localDir}`);
    const atmTests = readYamlFromPath(localDir);
    if (debugManager.isDebugEnabled())
      showInfo(`Successfully read: ${atmTests.length} Atm test files`);

    for (const { fileName, test } of atmTests) {
      if (debugManager.isDebugEnabled()) {
        showInfo(`Started generating tests for: ${fileName}`);
      }

      generateTest(test, output, fileName, debugManager);

      if (debugManager.isDebugEnabled()) {
        showInfo(`Completed generating tests for: ${fileName}`);
      }
    }
  } catch (error) {
    showError(String(error));
    process.exit(1);
  }
  showInfo(MIGRATION_COMPLETED);
};

export { migrateTestAction };
