import { PluginWarningsFact } from "./facts";

/**
 * Validates a Docker image reference format using the official Docker reference regex.
 * @param imageReference The Docker image reference to validate
 * @returns true if valid, false if invalid
 */
export function isValidDockerImageReference(imageReference: string): boolean {
  // Docker image reference validation regex from the official Docker packages:
  // https://github.com/distribution/reference/blob/ff14fafe2236e51c2894ac07d4bdfc778e96d682/regexp.go#L9
  // Original regex: ^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\[(?:[a-fA-F0-9:]+)\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$
  // Note: Converted [[:xdigit:]] to [a-fA-F0-9] and escaped the forward slashes for JavaScript compatibility.
  const dockerImageRegex =
    /^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\[(?:[a-fA-F0-9:]+)\])(?::[0-9]+)?\/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][a-fA-F0-9]{32,}))?$/;

  return dockerImageRegex.test(imageReference);
}

// array[*] indicates to truncate each element to the indicated size
export const RESPONSE_SIZE_LIMITS = {
  "containerConfig.data.user": { type: "string", limit: 1024 },
  "containerConfig.data.exposedPorts": { type: "array", limit: 500 },
  "containerConfig.data.exposedPorts[*]": { type: "string", limit: 64 },
  "containerConfig.data.env": { type: "array", limit: 500 },
  "containerConfig.data.env[*]": { type: "string", limit: 1024 },
  "containerConfig.data.entrypoint": { type: "array", limit: 500 },
  "containerConfig.data.entrypoint[*]": { type: "string", limit: 1024 },
  "containerConfig.data.cmd": { type: "array", limit: 500 },
  "containerConfig.data.cmd[*]": { type: "string", limit: 1024 },
  "containerConfig.data.volumes": { type: "array", limit: 500 },
  "containerConfig.data.volumes[*]": { type: "string", limit: 1024 },
  "containerConfig.data.workingDir": { type: "string", limit: 1024 },
  "containerConfig.data.stopSignal": { type: "string", limit: 128 },
  "history.data": { type: "array", limit: 1000 },
  "history.data[*].author": { type: "string", limit: 128 },
  "history.data[*].createdBy": { type: "string", limit: 4096 },
  "history.data[*].comment": { type: "string", limit: 4096 },
} as const;

interface TruncationInfo {
  type: "array" | "string";
  countAboveLimit: number;
}

export function truncateAdditionalFacts(facts: any[]): any[] {
  const truncationTracker: Record<string, TruncationInfo> = {};

  const processedFacts = facts.map((fact) => {
    if (!fact || !fact.type || !fact.data) {
      return fact;
    }
    if (fact.type === "depGraph") {
      return fact;
    }

    const truncatedData = truncateDataValue(
      fact.data,
      fact.type,
      "data",
      truncationTracker,
    );
    return { ...fact, data: truncatedData };
  });

  if (Object.keys(truncationTracker).length > 0) {
    const existingWarnings = processedFacts.find(
      (f) => f.type === "pluginWarnings",
    ) as PluginWarningsFact | undefined;

    if (existingWarnings) {
      existingWarnings.data.truncatedFacts = truncationTracker;
    } else {
      const pluginWarningsFact: PluginWarningsFact = {
        type: "pluginWarnings",
        data: {
          truncatedFacts: truncationTracker,
        },
      };
      processedFacts.push(pluginWarningsFact);
    }
  }

  return processedFacts;
}

function hasAnyLimitsForPath(factType: string, path: string): boolean {
  const prefix = `${factType}.${path}`;
  return Object.keys(RESPONSE_SIZE_LIMITS).some((limitKey) =>
    limitKey.startsWith(prefix),
  );
}

function truncateDataValue(
  value: any,
  factType: string,
  path: string,
  truncationTracker: Record<string, TruncationInfo>,
): any {
  const limitKey = `${factType}.${path}`;
  const limitConfig = RESPONSE_SIZE_LIMITS[limitKey];

  // directly truncate if there's a match
  if (limitConfig) {
    value = truncateValue(value, limitConfig, limitKey, truncationTracker);
  }

  if (!hasAnyLimitsForPath(factType, path)) {
    return value;
  }

  if (Array.isArray(value)) {
    return value.map((item, index) => {
      return truncateDataValue(item, factType, `${path}[*]`, truncationTracker);
    });
  } else if (typeof value === "object" && value !== null) {
    const truncatedObject: any = {};

    for (const [key, subValue] of Object.entries(value)) {
      truncatedObject[key] = truncateDataValue(
        subValue,
        factType,
        `${path}.${key}`,
        truncationTracker,
      );
    }
    return truncatedObject;
  }
  return value;
}

function truncateValue(
  value: any,
  limitConfig: any,
  fieldPath: string,
  truncationTracker: Record<string, TruncationInfo>,
): any {
  switch (limitConfig.type) {
    case "array":
      if (Array.isArray(value) && value.length > limitConfig.limit) {
        const truncatedCount = value.length - limitConfig.limit;
        // report how many elements were truncated
        truncationTracker[fieldPath] = {
          type: "array",
          countAboveLimit: truncatedCount,
        };
        return value.slice(0, limitConfig.limit);
      }
      break;
    case "string":
      if (typeof value === "string" && value.length > limitConfig.limit) {
        const truncatedCount = value.length - limitConfig.limit;
        // report the maximum number of characters that were truncated for this field
        const existing = truncationTracker[fieldPath];
        if (!existing || truncatedCount > existing.countAboveLimit) {
          truncationTracker[fieldPath] = {
            type: "string",
            countAboveLimit: truncatedCount,
          };
        }
        return value.substring(0, limitConfig.limit);
      }
      break;
  }
  return value;
}
