export interface IdeAPluginManifest {
  ideaPluginManifestVersion: 1;
  id: string;
  displayName: string;
  version: string;
  main: string;
  trustLevel: "full";
  description?: string;
  publisher?: string;
  engines?: IdeAPluginEngineConstraints;
  capabilities?: IdeAPluginCapability[];
  contributes?: {
    menus?: IdeAPluginTopLevelMenuContribution[];
    menuItems?: IdeAPluginMenuItemContribution[];
    layouts?: IdeAPluginLayoutContribution[];
    mcpServers?: IdeAPluginMcpServerContribution[];
  };
}

export type IdeAPluginCapability = "ui" | "mcp";

export interface IdeAPluginEngineConstraints {
  idea?: string;
}

export interface IdeAPluginTopLevelMenuContribution {
  id: string;
  label: string;
  topLevel: true;
  order?: number;
  icon?: string;
}

export interface IdeAPluginMenuItemContribution {
  id: string;
  targetMenuId: string;
  label: string;
  command: string;
  order?: number;
  icon?: string;
  when?: string;
}

export interface IdeAPluginLayoutContribution {
  type: string;
  label: string;
  component: string;
  order?: number;
  icon?: string;
  when?: string;
}

export interface IdeAPluginMcpServerContribution {
  id: string;
  displayName: string;
  command: string;
  args?: string[];
  env?: Record<string, string>;
  cwd?: string;
  transport: "stdio";
  autoStart?: boolean;
  allowAbsoluteCommand?: boolean;
}

export type PluginManifestValidationResult =
  | { success: true; data: IdeAPluginManifest; errors: [] }
  | { success: false; data?: undefined; errors: string[] };

const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/;
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;

export function validatePluginManifest(input: unknown): PluginManifestValidationResult {
  const errors: string[] = [];

  if (!isRecord(input)) {
    return { success: false, errors: ["manifest must be an object"] };
  }

  if (input.ideaPluginManifestVersion !== 1) {
    errors.push("ideaPluginManifestVersion must be 1");
  }

  requireString(input, "id", errors);
  requireString(input, "displayName", errors);
  requireString(input, "version", errors);
  requireString(input, "main", errors);
  if (input.trustLevel !== "full") {
    errors.push("trustLevel must be full");
  }
  optionalString(input, "description", errors);
  optionalString(input, "publisher", errors);

  if (typeof input.id === "string" && !PLUGIN_ID_PATTERN.test(input.id)) {
    errors.push("id must contain lowercase letters, digits, dots or dashes, and start/end with an alphanumeric character");
  }

  if (typeof input.version === "string" && !SEMVER_PATTERN.test(input.version)) {
    errors.push("version must use semver syntax, for example 0.1.0");
  }

  validateEngines(input.engines, errors);
  validateCapabilities(input.capabilities, errors);
  validateContributes(input.contributes, errors);

  if (errors.length > 0) {
    return { success: false, errors };
  }

  return { success: true, data: input as unknown as IdeAPluginManifest, errors: [] };
}

export function isPluginManifest(input: unknown): input is IdeAPluginManifest {
  return validatePluginManifest(input).success;
}

export function assertPluginManifest(input: unknown): asserts input is IdeAPluginManifest {
  const result = validatePluginManifest(input);

  if (!result.success) {
    throw new Error(`Invalid IdeA plugin manifest: ${result.errors.join("; ")}`);
  }
}

function validateEngines(value: unknown, errors: string[]): void {
  if (value === undefined) {
    return;
  }

  if (!isRecord(value)) {
    errors.push("engines must be an object when provided");
    return;
  }

  optionalString(value, "idea", errors, "engines.idea");
}

function validateCapabilities(value: unknown, errors: string[]): void {
  if (value === undefined) {
    return;
  }

  if (!Array.isArray(value)) {
    errors.push("capabilities must be an array when provided");
    return;
  }

  value.forEach((capability, index) => {
    if (capability !== "ui" && capability !== "mcp") {
      errors.push(`capabilities[${index}] must be "ui" or "mcp"`);
    }
  });
}

function validateContributes(value: unknown, errors: string[]): void {
  if (value === undefined) {
    return;
  }

  if (!isRecord(value)) {
    errors.push("contributes must be an object when provided");
    return;
  }

  validateArray(value, "menus", errors, (menu, index) => {
    requireString(menu, "id", errors, `contributes.menus[${index}].id`);
    requireString(menu, "label", errors, `contributes.menus[${index}].label`);
    if (menu.topLevel !== true) {
      errors.push(`contributes.menus[${index}].topLevel must be true`);
    }
    optionalNumber(menu, "order", errors, `contributes.menus[${index}].order`);
    optionalString(menu, "icon", errors, `contributes.menus[${index}].icon`);
  });
  validateArray(value, "menuItems", errors, (item, index) => {
    requireString(item, "id", errors, `contributes.menuItems[${index}].id`);
    requireString(item, "targetMenuId", errors, `contributes.menuItems[${index}].targetMenuId`);
    requireString(item, "label", errors, `contributes.menuItems[${index}].label`);
    requireString(item, "command", errors, `contributes.menuItems[${index}].command`);
    optionalNumber(item, "order", errors, `contributes.menuItems[${index}].order`);
    optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
    optionalString(item, "when", errors, `contributes.menuItems[${index}].when`);
  });
  validateArray(value, "layouts", errors, (layout, index) => {
    requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
    requireString(layout, "label", errors, `contributes.layouts[${index}].label`);
    requireString(layout, "component", errors, `contributes.layouts[${index}].component`);
    optionalNumber(layout, "order", errors, `contributes.layouts[${index}].order`);
    optionalString(layout, "icon", errors, `contributes.layouts[${index}].icon`);
    optionalString(layout, "when", errors, `contributes.layouts[${index}].when`);
  });
  validateArray(value, "mcpServers", errors, (server, index) => {
    requireString(server, "id", errors, `contributes.mcpServers[${index}].id`);
    requireString(server, "displayName", errors, `contributes.mcpServers[${index}].displayName`);
    requireString(server, "command", errors, `contributes.mcpServers[${index}].command`);
    if (server.transport !== "stdio") {
      errors.push(`contributes.mcpServers[${index}].transport must be "stdio"`);
    }
    optionalStringArray(server, "args", errors, `contributes.mcpServers[${index}].args`);
    optionalStringRecord(server, "env", errors, `contributes.mcpServers[${index}].env`);
    optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`);
    optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`);
    optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`);
  });
}

function validateArray(
  record: Record<string, unknown>,
  key: string,
  errors: string[],
  validateItem: (item: Record<string, unknown>, index: number) => void
): void {
  const value = record[key];
  if (value === undefined) {
    return;
  }

  if (!Array.isArray(value)) {
    errors.push(`contributes.${key} must be an array when provided`);
    return;
  }

  value.forEach((item, index) => {
    if (!isRecord(item)) {
      errors.push(`contributes.${key}[${index}] must be an object`);
      return;
    }

    validateItem(item, index);
  });
}

function requireString(
  record: Record<string, unknown>,
  key: string,
  errors: string[],
  label = key
): void {
  if (typeof record[key] !== "string" || record[key].trim().length === 0) {
    errors.push(`${label} must be a non-empty string`);
  }
}

function optionalString(
  record: Record<string, unknown>,
  key: string,
  errors: string[],
  label = key
): void {
  if (record[key] !== undefined && typeof record[key] !== "string") {
    errors.push(`${label} must be a string when provided`);
  }
}

function optionalNumber(
  record: Record<string, unknown>,
  key: string,
  errors: string[],
  label = key
): void {
  if (record[key] !== undefined && typeof record[key] !== "number") {
    errors.push(`${label} must be a number when provided`);
  }
}

function optionalBoolean(
  record: Record<string, unknown>,
  key: string,
  errors: string[],
  label = key
): void {
  if (record[key] !== undefined && typeof record[key] !== "boolean") {
    errors.push(`${label} must be a boolean when provided`);
  }
}

function optionalStringArray(
  record: Record<string, unknown>,
  key: string,
  errors: string[],
  label = key
): void {
  const value = record[key];
  if (value === undefined) {
    return;
  }

  if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
    errors.push(`${label} must be an array of strings when provided`);
  }
}

function optionalStringRecord(
  record: Record<string, unknown>,
  key: string,
  errors: string[],
  label = key
): void {
  const value = record[key];
  if (value === undefined) {
    return;
  }

  if (!isRecord(value) || Object.values(value).some((item) => typeof item !== "string")) {
    errors.push(`${label} must be an object of strings when provided`);
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
