import * as z from "zod";

const VALID_DIAGRAM_TYPES = [
  "bpmn",
  "er",
  "orgchart",
  "architecture",
  "sequence",
  "flowchart",
] as const;

const VALID_NODE_TYPES: Record<string, string[]> = {
  bpmn: [
    "activity",
    "subprocess",
    "start-event",
    "end-event",
    "event-timer",
    "event-message",
    "gateway-exclusive",
    "gateway-parallel",
    "gateway-inclusive",
    "data-object",
    "annotation",
  ],
  er: ["entity"],
  orgchart: ["person"],
  architecture: ["service", "database", "queue", "loadbalancer", "external"],
  sequence: ["participant", "fragment"],
  flowchart: ["process", "decision", "terminal", "io", "subprocess"],
};

const VALID_EDGE_TYPES: Record<string, string[] | null> = {
  bpmn: ["sequence", "message", "association"],
  er: null,
  orgchart: null,
  architecture: null,
  sequence: ["sync", "async", "return"],
  flowchart: null,
};

const MAX_NODES = 200;

const columnSchema = z.object({
  name: z.string(),
  type: z.string(),
  isPrimaryKey: z.boolean().optional(),
  isForeignKey: z.boolean().optional(),
  isNullable: z.boolean().optional(),
  isUnique: z.boolean().optional(),
  references: z.string().optional(),
});

const nodeSchema = z.object({
  id: z.string(),
  type: z.string(),
  label: z.string(),
  tag: z.string().optional(),
  icon: z.string().optional(),
  color: z.string().optional(),
  w: z.number().optional(),
  position: z
    .object({ x: z.number(), y: z.number() })
    .optional(),
  lane: z.string().optional(),
  group: z.string().optional(),
  columns: z.array(columnSchema).optional(),
  lifeline: z.boolean().optional(),
  parentId: z.string().optional(),
});

const edgeSchema = z.object({
  id: z.string(),
  from: z.string(),
  to: z.string(),
  label: z.string().optional(),
  color: z.string().optional(),
  type: z.string().optional(),
  cardinality: z.string().optional(),
});

const metaSchema = z.object({
  diagramType: z.enum(VALID_DIAGRAM_TYPES),
  title: z.string().min(1),
  description: z.string().optional(),
  version: z.string().optional(),
  layoutDirection: z.enum(["DOWN", "RIGHT", "LEFT", "UP"]).optional(),
  edgeRouting: z.enum(["ORTHOGONAL", "SPLINES", "POLYLINE"]).optional(),
});

const poolSchema = z.object({
  id: z.string(),
  label: z.string(),
  lanes: z.array(
    z.object({
      id: z.string(),
      label: z.string(),
    }),
  ),
});

const groupSchema = z.object({
  id: z.string(),
  label: z.string(),
  color: z.string().optional(),
});

export const graphPatchSchema = z.object({
  meta: metaSchema,
  nodes: z.array(nodeSchema).max(MAX_NODES),
  edges: z.array(edgeSchema),
  pools: z.array(poolSchema).optional(),
  groups: z.array(groupSchema).optional(),
});

export type GraphPatch = z.infer<typeof graphPatchSchema>;

/**
 * Validate that node types match the diagram type's allowed types.
 * Returns an array of error messages (empty if valid).
 */
export function validateNodeTypes(patch: GraphPatch): string[] {
  const diagramType = patch.meta.diagramType;
  const validTypes = VALID_NODE_TYPES[diagramType];
  if (!validTypes) return [];

  const errors: string[] = [];
  for (const node of patch.nodes) {
    const bare = node.type.includes(":") ? node.type.split(":")[1]! : node.type;
    if (!validTypes.includes(bare)) {
      errors.push(
        `Invalid node type "${node.type}" for ${diagramType}. Valid types: ${validTypes.join(", ")}`,
      );
    }
  }
  return errors;
}

/**
 * Validate that edge types match the diagram type's allowed types.
 * Returns an array of error messages (empty if valid).
 */
export function validateEdgeTypes(patch: GraphPatch): string[] {
  const diagramType = patch.meta.diagramType;
  const validTypes = VALID_EDGE_TYPES[diagramType];
  if (!validTypes) return [];

  const errors: string[] = [];
  for (const edge of patch.edges) {
    if (edge.type && !validTypes.includes(edge.type)) {
      errors.push(
        `Invalid edge type "${edge.type}" for ${diagramType}. Valid types: ${validTypes.join(", ")}`,
      );
    }
  }
  return errors;
}

/**
 * Validate that all edge from/to references point to existing node IDs.
 */
export function validateEdgeReferences(patch: GraphPatch): string[] {
  const nodeIds = new Set(patch.nodes.map((n) => n.id));
  const errors: string[] = [];
  for (const edge of patch.edges) {
    if (!nodeIds.has(edge.from)) {
      errors.push(`Edge "${edge.id}" references non-existent source node "${edge.from}"`);
    }
    if (!nodeIds.has(edge.to)) {
      errors.push(`Edge "${edge.id}" references non-existent target node "${edge.to}"`);
    }
  }
  return errors;
}

/**
 * Validate that all node and edge IDs are unique.
 */
export function validateUniqueIds(patch: GraphPatch): string[] {
  const errors: string[] = [];

  const nodeIds = new Set<string>();
  for (const node of patch.nodes) {
    if (nodeIds.has(node.id)) {
      errors.push(`Duplicate node ID "${node.id}"`);
    }
    nodeIds.add(node.id);
  }

  const edgeIds = new Set<string>();
  for (const edge of patch.edges) {
    if (edgeIds.has(edge.id)) {
      errors.push(`Duplicate edge ID "${edge.id}"`);
    }
    edgeIds.add(edge.id);
  }

  return errors;
}

/**
 * Full validation: schema + type-specific + referential integrity + unique IDs.
 * Returns { success: true, data } or { success: false, errors }.
 */
export function validateGraphPatch(
  input: unknown,
): { success: true; data: GraphPatch } | { success: false; errors: string[] } {
  const result = graphPatchSchema.safeParse(input);
  if (!result.success) {
    return {
      success: false,
      errors: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`),
    };
  }

  const patch = result.data;
  const typeErrors = [
    ...validateUniqueIds(patch),
    ...validateNodeTypes(patch),
    ...validateEdgeTypes(patch),
    ...validateEdgeReferences(patch),
  ];

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

  return { success: true, data: patch };
}
