import { getImportExportSpec } from "#/manifests/automation-interface";
import type {
  Automation,
  AutomationExportFile,
  AutomationSpec,
  AutomationTrigger,
} from "#/types/automation";

const SUPPORTED_TRIGGER_TYPES = new Set(["cron", "schedule", "event"]);

type UnknownRecord = Record<string, unknown>;

export class AutomationFileValidationError extends Error {
  readonly issues: string[];

  constructor(issues: string[]) {
    super(issues.join("\n"));
    this.name = "AutomationFileValidationError";
    this.issues = issues;
  }
}

function isRecord(value: unknown): value is UnknownRecord {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function validateRequiredString(
  record: UnknownRecord,
  field: string,
  path: string,
  issues: string[],
): string | undefined {
  const value = record[field];
  if (typeof value !== "string" || value.trim().length === 0) {
    issues.push(`${path}: expected a non-empty string`);
    return undefined;
  }
  return value;
}

function validateOptionalString(
  record: UnknownRecord,
  field: string,
  path: string,
  issues: string[],
): string | undefined {
  const value = record[field];
  if (value === undefined) return undefined;
  if (typeof value !== "string") {
    issues.push(`${path}: expected a string`);
    return undefined;
  }
  return value;
}

function validateTrigger(
  value: unknown,
  issues: string[],
): AutomationTrigger | undefined {
  if (!isRecord(value)) {
    issues.push("spec.trigger: expected an object");
    return undefined;
  }

  const type = validateRequiredString(
    value,
    "type",
    "spec.trigger.type",
    issues,
  );
  if (type && !SUPPORTED_TRIGGER_TYPES.has(type)) {
    issues.push(
      'spec.trigger.type: expected one of "cron", "schedule", or "event"',
    );
  }

  const schedule = validateOptionalString(
    value,
    "schedule",
    "spec.trigger.schedule",
    issues,
  );
  const scheduleHuman = validateOptionalString(
    value,
    "schedule_human",
    "spec.trigger.schedule_human",
    issues,
  );
  const timezone = validateOptionalString(
    value,
    "timezone",
    "spec.trigger.timezone",
    issues,
  );
  const source = validateOptionalString(
    value,
    "source",
    "spec.trigger.source",
    issues,
  );
  const filter = validateOptionalString(
    value,
    "filter",
    "spec.trigger.filter",
    issues,
  );

  let on: string | string[] | undefined;
  if (value.on !== undefined) {
    if (
      typeof value.on === "string" ||
      (Array.isArray(value.on) &&
        value.on.length > 0 &&
        value.on.every(
          (entry) => typeof entry === "string" && entry.trim().length > 0,
        ))
    ) {
      on = value.on as string | string[];
    } else {
      issues.push(
        "spec.trigger.on: expected a string or a non-empty array of strings",
      );
    }
  }

  if ((type === "cron" || type === "schedule") && !schedule?.trim()) {
    issues.push("spec.trigger.schedule: required for a scheduled trigger");
  }
  if (type === "event") {
    if (!source?.trim()) {
      issues.push("spec.trigger.source: required for an event trigger");
    }
    if (
      on === undefined ||
      (typeof on === "string" && on.trim().length === 0)
    ) {
      issues.push("spec.trigger.on: required for an event trigger");
    }
  }

  if (!type || !SUPPORTED_TRIGGER_TYPES.has(type)) return undefined;

  return {
    type,
    ...(schedule !== undefined && { schedule }),
    ...(scheduleHuman !== undefined && { schedule_human: scheduleHuman }),
    ...(timezone !== undefined && { timezone }),
    ...(source !== undefined && { source }),
    ...(on !== undefined && { on }),
    ...(filter !== undefined && { filter }),
  };
}

export function serializeAutomation(a: Automation): AutomationExportFile {
  const timezone = a.timezone ?? a.trigger.timezone;
  const spec: AutomationSpec = {
    name: a.name,
    trigger: { ...a.trigger },
    enabled: a.enabled,
    prompt: a.prompt,
    ...(a.repository !== undefined && { repository: a.repository }),
    ...(a.model !== undefined && { model: a.model }),
    ...(a.timeout != null && { timeout: a.timeout }),
    ...(a.branch !== undefined && { branch: a.branch }),
    ...(a.plugins !== undefined && { plugins: [...a.plugins] }),
    ...(a.notification !== undefined && { notification: a.notification }),
    ...(timezone !== undefined && { timezone }),
  };

  const envelope = getImportExportSpec();
  return { version: envelope.fileVersion, kind: envelope.fileKind, spec };
}

export function getAutomationExportFilename(
  automation: Pick<Automation, "id" | "name">,
): string {
  const slug = automation.name
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
  return `${slug || automation.id}${getImportExportSpec().filenameSuffix}`;
}

export function parseAutomationFile(json: unknown): AutomationSpec {
  const issues: string[] = [];
  if (!isRecord(json)) {
    throw new AutomationFileValidationError(["file: expected a JSON object"]);
  }

  const envelope = getImportExportSpec();
  if (json.version !== envelope.fileVersion) {
    issues.push(`version: expected ${envelope.fileVersion}`);
  }
  if (json.kind !== envelope.fileKind) {
    issues.push(`kind: expected "${envelope.fileKind}"`);
  }
  if (!isRecord(json.spec)) {
    issues.push("spec: expected an object");
    throw new AutomationFileValidationError(issues);
  }

  const name = validateRequiredString(json.spec, "name", "spec.name", issues);
  const prompt = validateRequiredString(
    json.spec,
    "prompt",
    "spec.prompt",
    issues,
  );
  const trigger = validateTrigger(json.spec.trigger, issues);

  const enabled = json.spec.enabled;
  if (typeof enabled !== "boolean") {
    issues.push("spec.enabled: expected a boolean");
  }

  const repository = validateOptionalString(
    json.spec,
    "repository",
    "spec.repository",
    issues,
  );
  const branch = validateOptionalString(
    json.spec,
    "branch",
    "spec.branch",
    issues,
  );
  const notification = validateOptionalString(
    json.spec,
    "notification",
    "spec.notification",
    issues,
  );
  const timezone = validateOptionalString(
    json.spec,
    "timezone",
    "spec.timezone",
    issues,
  );

  let model: string | null | undefined;
  if (json.spec.model === null || json.spec.model === undefined) {
    model = json.spec.model;
  } else if (
    typeof json.spec.model === "string" &&
    json.spec.model.trim().length > 0
  ) {
    model = json.spec.model;
  } else {
    issues.push("spec.model: expected a non-empty string or null");
  }

  let timeout: number | null | undefined;
  if (json.spec.timeout === null || json.spec.timeout === undefined) {
    timeout = json.spec.timeout;
  } else if (
    typeof json.spec.timeout === "number" &&
    Number.isInteger(json.spec.timeout) &&
    json.spec.timeout > 0
  ) {
    timeout = json.spec.timeout;
  } else {
    issues.push("spec.timeout: expected a positive integer");
  }

  let plugins: string[] | undefined;
  if (json.spec.plugins !== undefined) {
    if (
      Array.isArray(json.spec.plugins) &&
      json.spec.plugins.every(
        (plugin) => typeof plugin === "string" && plugin.trim().length > 0,
      )
    ) {
      plugins = [...json.spec.plugins] as string[];
    } else {
      issues.push("spec.plugins: expected an array of non-empty strings");
    }
  }

  if (issues.length > 0 || !name || !prompt || !trigger) {
    throw new AutomationFileValidationError(issues);
  }

  return {
    name,
    prompt,
    trigger,
    enabled: enabled as boolean,
    ...(repository !== undefined && { repository }),
    ...(model !== undefined && { model }),
    ...(timeout !== undefined && { timeout }),
    ...(branch !== undefined && { branch }),
    ...(plugins !== undefined && { plugins }),
    ...(notification !== undefined && { notification }),
    ...(timezone !== undefined && { timezone }),
  };
}
