import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  ChannelNotFoundError,
  DuplicateTemplateKeyError,
  EventTypeNotFoundError,
  MissingRequiredFieldError,
  UndeclaredVariableReferenceError,
} from "../lib/errors.generated";

export interface VariableSchemaEntry {
  name: string;
  type: string;
  required?: boolean;
}

export interface VariableSchemaShape {
  variables: VariableSchemaEntry[];
}

export interface CreateNotificationTemplateInput {
  eventType: string;
  channelId: string;
  locale: string;
  subject: string;
  body: string;
  htmlBody?: string | null;
  /** JSON-encoded VariableSchemaShape string */
  variableSchema: string;
}

const PLACEHOLDER_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;

export function extractPlaceholders(...sources: Array<string | null | undefined>): string[] {
  const found = new Set<string>();
  for (const source of sources) {
    if (!source) continue;
    for (const match of source.matchAll(PLACEHOLDER_PATTERN)) {
      found.add(match[1]);
    }
  }
  return [...found];
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object";
}

export function parseVariableSchema(raw: string): VariableSchemaShape | null {
  try {
    const parsed: unknown = JSON.parse(raw);
    if (!isRecord(parsed)) return null;
    const rawVariables: unknown = parsed.variables;
    if (!Array.isArray(rawVariables)) return null;
    const variables: VariableSchemaEntry[] = [];
    for (const rawEntry of rawVariables) {
      const entry: unknown = rawEntry;
      if (!isRecord(entry)) return null;
      if (typeof entry.name !== "string" || typeof entry.type !== "string") return null;
      variables.push({
        name: entry.name,
        type: entry.type,
        required: typeof entry.required === "boolean" ? entry.required : false,
      });
    }
    return { variables };
  } catch {
    return null;
  }
}

/**
 * Inserts a NotificationTemplate for an (eventType, channelId, locale) triple after validating the catalog event type, channel, variable schema, and placeholder references.
 */
export async function run(
  db: Transaction,
  input: CreateNotificationTemplateInput,
  _ctx: CommandContext,
) {
  const { eventType, channelId, locale, subject, body, htmlBody, variableSchema } = input;

  if (!eventType) return err(new MissingRequiredFieldError("eventType"));
  if (!channelId) return err(new MissingRequiredFieldError("channelId"));
  if (!locale) return err(new MissingRequiredFieldError("locale"));

  const trimmedSubject = subject?.trim() ?? "";
  if (trimmedSubject === "") return err(new MissingRequiredFieldError("subject"));

  const trimmedBody = body?.trim() ?? "";
  if (trimmedBody === "") return err(new MissingRequiredFieldError("body"));

  const trimmedHtmlBody = htmlBody?.trim();
  if (htmlBody !== undefined && htmlBody !== null && trimmedHtmlBody === "") {
    return err(new MissingRequiredFieldError("htmlBody"));
  }

  if (!variableSchema) return err(new MissingRequiredFieldError("variableSchema"));

  const schema = parseVariableSchema(variableSchema);
  if (!schema) return err(new MissingRequiredFieldError("variableSchema"));

  const eventBinding = await db
    .selectFrom("EventCategoryBinding")
    .select("id")
    .where("eventType", "=", eventType)
    .executeTakeFirst();
  if (!eventBinding) return err(new EventTypeNotFoundError(eventType));

  const channel = await db
    .selectFrom("NotificationChannel")
    .select("id")
    .where("id", "=", channelId)
    .executeTakeFirst();
  if (!channel) return err(new ChannelNotFoundError(channelId));

  const existing = await db
    .selectFrom("NotificationTemplate")
    .select("id")
    .where("eventType", "=", eventType)
    .where("channelId", "=", channelId)
    .where("locale", "=", locale)
    .executeTakeFirst();
  if (existing) {
    return err(new DuplicateTemplateKeyError(`${eventType}:${channelId}:${locale}`));
  }

  const declared = new Set(schema.variables.map((v) => v.name));
  const referenced = extractPlaceholders(subject, body, htmlBody ?? null);
  for (const ref of referenced) {
    if (!declared.has(ref)) {
      return err(new UndeclaredVariableReferenceError(ref));
    }
  }

  const now = new Date();
  const template = await db
    .insertInto("NotificationTemplate")
    .values({
      id: crypto.randomUUID(),
      eventType,
      channelId,
      locale,
      subject,
      body,
      htmlBody: htmlBody ?? null,
      variableSchema,
      createdAt: now,
      updatedAt: now,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ template });
}
