import { type ClassValue, clsx } from "clsx";
import _isEmpty from "lodash/isEmpty";
import { twMerge } from "tailwind-merge";

import { NodeDimension } from "@/app/(platform)/build/components/legacy-builder/Flow/Flow";
import {
  BlockIOObjectSubSchema,
  BlockIORootSchema,
  BlockIOSubSchema,
  Category,
  GraphInputSubSchema,
  GraphOutputSubSchema,
} from "@/lib/autogpt-server-api/types";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

/** Derived from https://stackoverflow.com/a/7616484 */
export function hashString(str: string): number {
  let hash = 0,
    chr: number;
  if (str.length === 0) return hash;
  for (let i = 0; i < str.length; i++) {
    chr = str.charCodeAt(i);
    hash = (hash << 5) - hash + chr;
    hash |= 0; // Convert to 32bit integer
  }
  return hash;
}

/** Derived from https://stackoverflow.com/a/32922084 */
export function deepEquals(x: any, y: any): boolean {
  const ok = (obj: any) => Object.keys(obj).filter((key) => obj[key] !== null),
    tx = typeof x,
    ty = typeof y;

  const res =
    x && y && tx === ty && tx === "object"
      ? ok(x).length === ok(y).length &&
        ok(x).every((key) => deepEquals(x[key], y[key]))
      : x === y;
  return res;
}

/** Get tailwind text color class from type name */
export function getTypeTextColor(type: string | null): string {
  if (type === null) return "text-gray-500";
  return (
    {
      string: "text-green-500",
      number: "text-blue-500",
      integer: "text-blue-500",
      boolean: "text-yellow-500",
      object: "text-purple-500",
      array: "text-indigo-500",
      null: "text-gray-500",
      any: "text-gray-500",
      "": "text-gray-500",
    }[type] || "text-gray-500"
  );
}

/** Get tailwind bg color class from type name */
export function getTypeBgColor(type: string | null): string {
  if (type === null) return "border-gray-500";
  return (
    {
      string: "border-green-500",
      number: "border-blue-500",
      integer: "border-blue-500",
      boolean: "border-yellow-500",
      object: "border-purple-500",
      array: "border-indigo-500",
      null: "border-gray-500",
      any: "border-gray-500",
      "": "border-gray-500",
    }[type] || "border-gray-500"
  );
}

export function getTypeColor(type: string | undefined): string {
  if (!type) return "#6b7280";
  return (
    {
      string: "#22c55e",
      number: "#3b82f6",
      integer: "#3b82f6",
      boolean: "#eab308",
      object: "#a855f7",
      array: "#6366f1",
      null: "#6b7280",
      any: "#6b7280",
    }[type] || "#6b7280"
  );
}

/**
 * Extracts the effective type from a JSON schema, handling anyOf/oneOf/allOf wrappers.
 * Returns the first non-null type found in the schema structure.
 */
export function getEffectiveType(
  schema:
    | BlockIOSubSchema
    | GraphInputSubSchema
    | GraphOutputSubSchema
    | null
    | undefined,
): string | undefined {
  if (!schema) return undefined;

  // Direct type property
  if ("type" in schema && schema.type) {
    return String(schema.type);
  }

  // Handle allOf - typically a single-item wrapper
  if (
    "allOf" in schema &&
    Array.isArray(schema.allOf) &&
    schema.allOf.length > 0
  ) {
    return getEffectiveType(schema.allOf[0]);
  }

  // Handle anyOf - e.g. [{ type: "string" }, { type: "null" }]
  if ("anyOf" in schema && Array.isArray(schema.anyOf)) {
    for (const item of schema.anyOf) {
      if ("type" in item && item.type !== "null") {
        return String(item.type);
      }
    }
  }

  // Handle oneOf
  if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
    for (const item of schema.oneOf) {
      if ("type" in item && item.type !== "null") {
        return String(item.type);
      }
    }
  }

  return undefined;
}

export function beautifyString(name: string): string {
  // Regular expression to identify places to split, considering acronyms
  const result = name
    .replace(/([a-z])([A-Z])/g, "$1 $2") // Add space before capital letters
    .replace(/([A-Z])([A-Z][a-z])/g, "$1 $2") // Add space between acronyms and next word
    .replace(/_/g, " ") // Replace underscores with spaces
    .replace(/\b\w/g, (char) => char.toUpperCase()); // Capitalize the first letter of each word

  return applyExceptions(result);
}

const exceptionMap: Record<string, string> = {
  "Auto GPT": "AutoGPT",
  Gpt: "GPT",
  Creds: "Credentials",
  Id: "ID",
  Openai: "OpenAI",
  Api: "API",
  Url: "URL",
  Http: "HTTP",
  Json: "JSON",
  Ai: "AI",
  "You Tube": "YouTube",
};

const applyExceptions = (str: string): string => {
  Object.keys(exceptionMap).forEach((key) => {
    const regex = new RegExp(`\\b${key}\\b`, "g");
    str = str.replace(regex, exceptionMap[key]);
  });
  return str;
};

export function exportAsJSONFile(obj: object, filename: string): void {
  // Create downloadable blob
  const jsonString = JSON.stringify(obj, null, 2);
  const blob = new Blob([jsonString], { type: "application/json" });
  const url = URL.createObjectURL(blob);

  // Trigger the browser to download the blob to a file
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);

  // Clean up
  URL.revokeObjectURL(url);
}

export function setNestedProperty(obj: any, path: string, value: any) {
  if (!obj || typeof obj !== "object") {
    throw new Error("Target must be a non-null object");
  }

  if (!path || typeof path !== "string") {
    throw new Error("Path must be a non-empty string");
  }

  // Split by both / and . to handle mixed separators, then filter empty strings
  const keys = path.split(/[\/.]/).filter((key) => key.length > 0);

  if (keys.length === 0) {
    throw new Error("Path must be a non-empty string");
  }

  // Validate keys for prototype pollution protection
  for (const key of keys) {
    if (key === "__proto__" || key === "constructor" || key === "prototype") {
      throw new Error(`Invalid property name: ${key}`);
    }
  }

  // Securely traverse and set nested properties
  // Use Object.prototype.hasOwnProperty.call() to safely check properties
  let current = obj;

  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i];

    // Use hasOwnProperty check to avoid prototype chain access
    if (!Object.prototype.hasOwnProperty.call(current, key)) {
      current[key] = {};
    } else if (typeof current[key] !== "object" || current[key] === null) {
      current[key] = {};
    }
    current = current[key];
  }

  // Set the final value using bracket notation with validated key
  // Since we've validated all keys, this is safe from prototype pollution
  const finalKey = keys[keys.length - 1];
  current[finalKey] = value;
}

export function pruneEmptyValues(
  obj: any,
  removeEmptyStrings: boolean = true,
): any {
  if (Array.isArray(obj)) {
    // If obj is an array, recursively check each element,
    // but element removal is avoided to prevent index changes.
    return obj.map((item) =>
      item === undefined || item === null
        ? ""
        : pruneEmptyValues(item, removeEmptyStrings),
    );
  } else if (typeof obj === "object" && obj !== null) {
    // If obj is an object, recursively remove empty strings and nulls from its properties
    for (const key in obj) {
      if (!obj.hasOwnProperty(key)) continue;

      const value = obj[key];
      if (
        value === null ||
        value === undefined ||
        (typeof value === "string" && value === "" && removeEmptyStrings)
      ) {
        delete obj[key];
      } else if (typeof value === "object") {
        obj[key] = pruneEmptyValues(value, removeEmptyStrings);
      }
    }
  }
  return obj;
}

export function fillObjectDefaultsFromSchema(
  obj: Record<any, any>,
  schema: BlockIORootSchema | BlockIOObjectSubSchema,
) {
  for (const key in schema.properties) {
    if (!schema.properties.hasOwnProperty(key)) continue;

    const propertySchema = schema.properties[key];

    if ("default" in propertySchema && propertySchema.default !== undefined) {
      // Apply simple default values
      obj[key] ??= propertySchema.default;
    } else if (
      propertySchema.type === "object" &&
      "properties" in propertySchema
    ) {
      // Recursively fill defaults for nested objects
      obj[key] = fillObjectDefaultsFromSchema(obj[key] ?? {}, propertySchema);
    } else if (propertySchema.type === "array") {
      obj[key] ??= [];
      // If the array items are objects, fill their defaults as well
      if (
        Array.isArray(obj[key]) &&
        propertySchema.items?.type === "object" &&
        "properties" in propertySchema.items
      ) {
        for (const item of obj[key]) {
          if (typeof item === "object" && item !== null) {
            fillObjectDefaultsFromSchema(item, propertySchema.items);
          }
        }
      }
    }
  }

  return obj;
}

export const categoryColorMap: Record<string, string> = {
  AI: "bg-orange-300 dark:bg-orange-700",
  SOCIAL: "bg-yellow-300 dark:bg-yellow-700",
  TEXT: "bg-green-300 dark:bg-green-700",
  SEARCH: "bg-blue-300 dark:bg-blue-700",
  BASIC: "bg-purple-300 dark:bg-purple-700",
  INPUT: "bg-cyan-300 dark:bg-cyan-700",
  OUTPUT: "bg-red-300 dark:bg-red-700",
  LOGIC: "bg-teal-300 dark:bg-teal-700",
  DEVELOPER_TOOLS: "bg-fuchsia-300 dark:bg-fuchsia-700",
  AGENT: "bg-lime-300 dark:bg-lime-700",
};

export function getPrimaryCategoryColor(categories: Category[]): string {
  if (categories.length === 0) {
    return "bg-gray-300 dark:bg-slate-700";
  }
  return (
    categoryColorMap[categories[0].category] || "bg-gray-300 dark:bg-slate-700"
  );
}

function rectanglesOverlap(
  rect1: { x: number; y: number; width: number; height?: number },
  rect2: { x: number; y: number; width: number; height?: number },
): boolean {
  const x1 = rect1.x,
    y1 = rect1.y,
    w1 = rect1.width,
    h1 = rect1.height ?? 100;
  const x2 = rect2.x,
    y2 = rect2.y,
    w2 = rect2.width,
    h2 = rect2.height ?? 100;

  // Check if the rectangles do not overlap
  return !(x1 + w1 <= x2 || x1 >= x2 + w2 || y1 + h1 <= y2 || y1 >= y2 + h2);
}

export function findNewlyAddedBlockCoordinates(
  nodeDimensions: NodeDimension,
  newWidth: number,
  margin: number,
  zoom: number,
) {
  const nodeDimensionArray = Object.values(nodeDimensions);

  for (let i = nodeDimensionArray.length - 1; i >= 0; i--) {
    const lastNode = nodeDimensionArray[i];
    const lastNodeHeight = lastNode.height ?? 100;

    // Right of the last node
    let newX = lastNode.x + lastNode.width + margin;
    let newY = lastNode.y;
    let newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };

    const collisionRight = nodeDimensionArray.some((node) =>
      rectanglesOverlap(newRect, node),
    );

    if (!collisionRight) {
      return { x: newX, y: newY };
    }

    // Left of the last node
    newX = lastNode.x - newWidth - margin;
    newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };

    const collisionLeft = nodeDimensionArray.some((node) =>
      rectanglesOverlap(newRect, node),
    );

    if (!collisionLeft) {
      return { x: newX, y: newY };
    }

    // Below the last node
    newX = lastNode.x;
    newY = lastNode.y + lastNodeHeight + margin;
    newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };

    const collisionBelow = nodeDimensionArray.some((node) =>
      rectanglesOverlap(newRect, node),
    );

    if (!collisionBelow) {
      return { x: newX, y: newY };
    }
  }

  // Default position if no space is found
  return {
    x: 0,
    y: 0,
  };
}

export function hasNonNullNonObjectValue(obj: any): boolean {
  if (obj !== null && typeof obj === "object") {
    return Object.values(obj).some((value) => hasNonNullNonObjectValue(value));
  } else {
    return obj !== null && typeof obj !== "object";
  }
}

type ParsedKey = { key: string; index?: number };

export function parseKeys(key: string): ParsedKey[] {
  const splits = key.split(/_@_|_#_|_\$_|\./);
  const keys: ParsedKey[] = [];
  let currentKey: string | null = null;

  splits.forEach((split) => {
    const isInteger = /^\d+$/.test(split);
    if (!isInteger) {
      if (currentKey !== null) {
        keys.push({ key: currentKey });
      }
      currentKey = split;
    } else {
      if (currentKey !== null) {
        keys.push({ key: currentKey, index: parseInt(split, 10) });
        currentKey = null;
      } else {
        throw new Error("Invalid key format: array index without a key");
      }
    }
  });

  if (currentKey !== null) {
    keys.push({ key: currentKey });
  }

  return keys;
}

/**
 * Get the value of a nested key in an object, handles arrays and objects.
 */
export function getValue(key: string, value: any) {
  const keys = parseKeys(key);
  return keys.reduce((acc, k) => {
    if (acc === undefined) return undefined;
    if (k.index !== undefined) {
      return Array.isArray(acc[k.key]) ? acc[k.key][k.index] : undefined;
    }
    return acc[k.key];
  }, value);
}

/** Check if a string is empty or whitespace */
export function isEmptyOrWhitespace(str: string | undefined | null): boolean {
  return !str || str.trim().length === 0;
}

export function isEmpty(value: any): boolean {
  return (
    value === undefined ||
    value === "" ||
    (typeof value === "object" &&
      (value instanceof Date ? isNaN(value.getTime()) : _isEmpty(value))) ||
    (typeof value === "number" && isNaN(value))
  );
}

/** Check if a value is an object or not */
export function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

/** Validate YouTube URL */
export function validateYouTubeUrl(val: string): boolean {
  if (!val) return true;
  try {
    const url = new URL(val);
    const allowedHosts = [
      "youtube.com",
      "www.youtube.com",
      "youtu.be",
      "www.youtu.be",
    ];
    return allowedHosts.includes(url.hostname);
  } catch {
    return false;
  }
}

export function isValidUUID(value: string): boolean {
  const uuidRegex =
    /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
  return uuidRegex.test(value);
}
