import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import { execFile } from "child_process";
import { promisify } from "util";
import jwt from "jsonwebtoken";

const execFileAsync = promisify(execFile);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const DOCUMENTS_ROOT = path.resolve(__dirname, "../../documents");
const CURL_RUNNER_PASSWORD = process.env.CURL_RUNNER_PASSWORD;
const CURL_RUNNER_JWT_SECRET =
  process.env.CURL_RUNNER_JWT_SECRET || "omidib-curl-runner-secret";
const CURL_RUNNER_TOKEN_HEADER = "x-curl-runner-token";
const CURL_META_MARKER = "__OMIDIB_CURL_RUNNER_META__";
const CURL_TIMEOUT_SECONDS = 45;

const CURL_OPTIONS_WITH_VALUE = new Set([
  "-A",
  "-b",
  "-c",
  "-d",
  "-D",
  "-H",
  "-o",
  "-e",
  "-T",
  "--upload-file",
  "-u",
  "--user",
  "-w",
  "-X",
  "-x",
  "-Y",
  "-y",
  "-z",
  "--connect-timeout",
  "--cookie",
  "--cookie-jar",
  "--data",
  "--data-ascii",
  "--data-binary",
  "--data-raw",
  "--data-urlencode",
  "--dump-header",
  "--form",
  "--form-string",
  "--header",
  "--json",
  "--max-time",
  "--output",
  "--proxy",
  "--proxy-header",
  "--referer",
  "--request",
  "--resolve",
  "--retry",
  "--retry-delay",
  "--retry-max-time",
  "--speed-limit",
  "--speed-time",
  "--time-cond",
  "--url",
  "--user-agent",
  "--write-out",
]);

const DISALLOWED_CURL_FLAGS = new Set([
  "-K",
  "--config",
  "-o",
  "--output",
  "-O",
  "--remote-name",
  "--remote-name-all",
  "-J",
  "--remote-header-name",
  "--output-dir",
  "--create-dirs",
  "-D",
  "--dump-header",
  "--trace",
  "--trace-ascii",
  "--trace-config",
  "--libcurl",
]);

const LOCAL_FILE_VALUE_FLAGS = new Set([
  "-d",
  "--data",
  "--data-ascii",
  "--data-binary",
  "--data-urlencode",
  "-F",
  "--form",
  "-T",
  "--upload-file",
]);

const parseShellTokens = (command) => {
  const normalizedCommand = command.replace(/\\\r?\n/g, " ").trim();
  const tokens = [];
  let currentToken = "";
  let activeQuote = null;
  let isEscaping = false;

  for (const character of normalizedCommand) {
    if (activeQuote === "single") {
      if (character === "'") {
        activeQuote = null;
      } else {
        currentToken += character;
      }
      continue;
    }

    if (isEscaping) {
      currentToken += character;
      isEscaping = false;
      continue;
    }

    if (activeQuote === "double") {
      if (character === '"') {
        activeQuote = null;
      } else if (character === "\\") {
        isEscaping = true;
      } else {
        currentToken += character;
      }
      continue;
    }

    if (character === "'") {
      activeQuote = "single";
      continue;
    }

    if (character === '"') {
      activeQuote = "double";
      continue;
    }

    if (character === "\\") {
      isEscaping = true;
      continue;
    }

    if (/\s/.test(character)) {
      if (currentToken) {
        tokens.push(currentToken);
        currentToken = "";
      }
      continue;
    }

    currentToken += character;
  }

  if (activeQuote || isEscaping) {
    throw new Error("Curl command has an unfinished quote or escape sequence.");
  }

  if (currentToken) {
    tokens.push(currentToken);
  }

  return tokens;
};

const getFlagName = (token) => {
  if (!token.startsWith("-")) {
    return null;
  }

  if (token.startsWith("--")) {
    return token.split("=")[0];
  }

  if (token.length === 2) {
    return token;
  }

  return null;
};

const getInlineFlagValue = (token) => {
  if (!token.startsWith("--")) {
    return null;
  }

  const separatorIndex = token.indexOf("=");

  if (separatorIndex === -1) {
    return null;
  }

  return token.slice(separatorIndex + 1);
};

const assertAllowedCurlValue = (flagName, value) => {
  if (!value) {
    return;
  }

  if (flagName === "--url" && value.startsWith("file://")) {
    throw new Error("Local file URLs are not allowed in curl runner.");
  }

  if (LOCAL_FILE_VALUE_FLAGS.has(flagName)) {
    if (value.startsWith("@") || value.includes("=@")) {
      throw new Error(
        "Reading request data from local files is not allowed in curl runner.",
      );
    }
  }
};

const prepareCurlArgs = (command) => {
  const tokens = parseShellTokens(command);

  if (tokens.length === 0) {
    throw new Error("Curl command is required.");
  }

  if (tokens[0] !== "curl") {
    throw new Error("Only curl commands are allowed.");
  }

  const args = [];
  let hasMaxTime = false;
  let hasSilent = false;
  let hasShowError = false;
  let foundUrl = false;

  for (let index = 1; index < tokens.length; index += 1) {
    const token = tokens[index];
    const flagName = getFlagName(token);
    const inlineValue = getInlineFlagValue(token);

    if (flagName && DISALLOWED_CURL_FLAGS.has(flagName)) {
      throw new Error(`The curl option "${flagName}" is not allowed here.`);
    }

    if (flagName === "--max-time") {
      hasMaxTime = true;
    }

    if (flagName === "-s" || flagName === "--silent") {
      hasSilent = true;
    }

    if (flagName === "-S" || flagName === "--show-error") {
      hasShowError = true;
    }

    if (flagName) {
      if (inlineValue !== null) {
        assertAllowedCurlValue(flagName, inlineValue);
        if (flagName === "--url" && /^https?:\/\//i.test(inlineValue)) {
          foundUrl = true;
        }
        args.push(token);
        continue;
      }

      if (CURL_OPTIONS_WITH_VALUE.has(flagName)) {
        const nextValue = tokens[index + 1];

        if (nextValue === undefined) {
          throw new Error(`The curl option "${flagName}" needs a value.`);
        }

        assertAllowedCurlValue(flagName, nextValue);

        if (flagName === "--url" && /^https?:\/\//i.test(nextValue)) {
          foundUrl = true;
        }

        args.push(token, nextValue);
        index += 1;
        continue;
      }
    }

    if (/^https?:\/\//i.test(token)) {
      foundUrl = true;
    }

    if (token.startsWith("file://")) {
      throw new Error("Local file URLs are not allowed in curl runner.");
    }

    args.push(token);
  }

  if (!foundUrl) {
    throw new Error("Curl command must include an http or https URL.");
  }

  if (!hasSilent) {
    args.push("--silent");
  }

  if (!hasShowError) {
    args.push("--show-error");
  }

  if (!hasMaxTime) {
    args.push("--max-time", String(CURL_TIMEOUT_SECONDS));
  }

  args.push(
    "--write-out",
    `\n${CURL_META_MARKER}http_status=%{http_code};content_type=%{content_type};url=%{url_effective}`,
  );

  return args;
};

const parseCurlMetadata = (stdout = "") => {
  const markerIndex = stdout.lastIndexOf(`\n${CURL_META_MARKER}`);

  if (markerIndex === -1) {
    return {
      responseBody: stdout,
      httpStatus: null,
      contentType: null,
      effectiveUrl: null,
    };
  }

  const responseBody = stdout.slice(0, markerIndex);
  const metadataString = stdout
    .slice(markerIndex + `\n${CURL_META_MARKER}`.length)
    .trim();
  const metadata = Object.fromEntries(
    metadataString
      .split(";")
      .map((item) => {
        const separatorIndex = item.indexOf("=");
        return [item.slice(0, separatorIndex), item.slice(separatorIndex + 1)];
      })
      .filter(([key]) => Boolean(key)),
  );

  return {
    responseBody,
    httpStatus: metadata.http_status || null,
    contentType: metadata.content_type || null,
    effectiveUrl: metadata.url || null,
  };
};

const buildCurlSummary = (command) => {
  try {
    const tokens = parseShellTokens(command);
    const explicitUrlIndex = tokens.indexOf("--url");
    const urlToken =
      tokens.find((token) => /^https?:\/\//i.test(token)) ||
      (explicitUrlIndex !== -1 ? tokens[explicitUrlIndex + 1] : null) ||
      null;
    const requestIndex =
      tokens.indexOf("-X") !== -1
        ? tokens.indexOf("-X")
        : tokens.indexOf("--request");
    const method =
      requestIndex !== -1 ? tokens[requestIndex + 1]?.toUpperCase() : "GET";

    return {
      method,
      url: urlToken,
    };
  } catch (error) {
    return {
      method: "GET",
      url: null,
    };
  }
};

const collectDocumentFiles = async () => {
  const entries = await fs.readdir(DOCUMENTS_ROOT, { withFileTypes: true });
  const markdownFiles = [];

  for (const entry of entries) {
    if (!entry.isDirectory()) {
      continue;
    }

    const directoryPath = path.join(DOCUMENTS_ROOT, entry.name);
    const children = await fs.readdir(directoryPath, { withFileTypes: true });

    children
      .filter(
        (child) => child.isFile() && child.name.toLowerCase().endsWith(".md"),
      )
      .forEach((child) => {
        markdownFiles.push({
          category: entry.name,
          absolutePath: path.join(directoryPath, child.name),
          relativePath: path.posix.join(entry.name, child.name),
        });
      });
  }

  return markdownFiles.sort((left, right) =>
    left.relativePath.localeCompare(right.relativePath),
  );
};

const parseMarkdownPresets = (content, fileInfo) => {
  const presets = [];
  const lines = content.split(/\r?\n/);
  const headings = [];
  let isInsideCodeFence = false;
  let codeFenceLanguage = "";
  let codeBlockLines = [];
  let presetCounter = 0;

  const addPresetFromBlock = () => {
    const command = codeBlockLines.join("\n").trim();

    if (!command.startsWith("curl ")) {
      return;
    }

    const title =
      headings[headings.length - 1]?.text ||
      `${fileInfo.category} preset ${presetCounter + 1}`;
    const summary = buildCurlSummary(command);

    presets.push({
      id: `${fileInfo.category}-${presetCounter + 1}`,
      title,
      category: fileInfo.category,
      document: fileInfo.relativePath,
      command,
      method: summary.method,
      url: summary.url,
    });

    presetCounter += 1;
  };

  for (const line of lines) {
    const codeFenceMatch = line.match(/^```([\w-]*)\s*$/);

    if (codeFenceMatch) {
      if (!isInsideCodeFence) {
        isInsideCodeFence = true;
        codeFenceLanguage = codeFenceMatch[1].toLowerCase();
        codeBlockLines = [];
      } else {
        const isShellBlock =
          !codeFenceLanguage ||
          ["bash", "shell", "sh"].includes(codeFenceLanguage);

        if (isShellBlock) {
          addPresetFromBlock();
        }

        isInsideCodeFence = false;
        codeFenceLanguage = "";
        codeBlockLines = [];
      }

      continue;
    }

    if (isInsideCodeFence) {
      codeBlockLines.push(line);
      continue;
    }

    const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);

    if (headingMatch) {
      const level = headingMatch[1].length;
      const text = headingMatch[2].trim();

      headings.splice(level);
      headings[level - 1] = { level, text };
    }
  }

  return presets;
};

const getPresetDocuments = async () => {
  const files = await collectDocumentFiles();
  const presets = [];

  for (const fileInfo of files) {
    const content = await fs.readFile(fileInfo.absolutePath, "utf8");
    presets.push(...parseMarkdownPresets(content, fileInfo));
  }

  return presets;
};

const createRunnerToken = () =>
  jwt.sign({ scope: "curl-runner" }, CURL_RUNNER_JWT_SECRET, {
    expiresIn: "8h",
  });

const verifyRunnerToken = (req) => {
  const token = req.header(CURL_RUNNER_TOKEN_HEADER);

  if (!token) {
    const error = new Error("Curl runner login is required.");
    error.statusCode = 401;
    throw error;
  }

  try {
    const payload = jwt.verify(token, CURL_RUNNER_JWT_SECRET);

    if (payload.scope !== "curl-runner") {
      throw new Error("Invalid token scope");
    }
  } catch (error) {
    const authError = new Error("Curl runner token is invalid or expired.");
    authError.statusCode = 401;
    throw authError;
  }
};

export const CURL_LOGIN = async (req, res) => {
  try {
    const { password } = req.body || {};

    if (password !== CURL_RUNNER_PASSWORD) {
      return res.status(401).send({
        message: "Password is incorrect.",
      });
    }

    return res.status(200).send({
      token: createRunnerToken(),
    });
  } catch (error) {
    return res.status(500).send({
      message: error.message,
    });
  }
};

export const CURL_PRESETS = async (req, res) => {
  try {
    verifyRunnerToken(req);
    const presets = await getPresetDocuments();

    return res.status(200).send({
      presets,
    });
  } catch (error) {
    return res.status(error.statusCode || 500).send({
      message: error.message,
    });
  }
};

export const CURL_RUN = async (req, res) => {
  try {
    verifyRunnerToken(req);

    const { command } = req.body || {};
    const preparedArgs = prepareCurlArgs(command || "");
    const startedAt = Date.now();

    try {
      const { stdout, stderr } = await execFileAsync("curl", preparedArgs, {
        timeout: (CURL_TIMEOUT_SECONDS + 5) * 1000,
        maxBuffer: 4 * 1024 * 1024,
      });
      const metadata = parseCurlMetadata(stdout);

      return res.status(200).send({
        ok: true,
        command,
        exitCode: 0,
        durationMs: Date.now() - startedAt,
        stdout,
        stderr,
        responseBody: metadata.responseBody,
        httpStatus: metadata.httpStatus,
        contentType: metadata.contentType,
        effectiveUrl: metadata.effectiveUrl,
      });
    } catch (executionError) {
      const metadata = parseCurlMetadata(executionError.stdout || "");

      return res.status(200).send({
        ok: false,
        command,
        exitCode: executionError.code ?? null,
        durationMs: Date.now() - startedAt,
        stdout: executionError.stdout || "",
        stderr: executionError.stderr || executionError.message,
        responseBody: metadata.responseBody,
        httpStatus: metadata.httpStatus,
        contentType: metadata.contentType,
        effectiveUrl: metadata.effectiveUrl,
        errorMessage: executionError.message,
      });
    }
  } catch (error) {
    return res.status(error.statusCode || 500).send({
      message: error.message,
    });
  }
};
