import type {
  ContractErrorDefinition,
  ContractErrorResponses,
  ContractMeta,
  ContractResponses,
} from "./types.js";
import { STANDARD_ERROR_RESPONSE_SCHEMA } from "./types.js";

function isContractErrorDefinition(
  value: unknown,
): value is ContractErrorDefinition {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as { code?: unknown }).code === "string" &&
    typeof (value as { status?: unknown }).status === "number" &&
    typeof (value as { message?: unknown }).message === "string"
  );
}

/**
 * Extract catalog error definitions declared in contract metadata.
 */
export function catalogErrorsFromMeta(
  metadata: ContractMeta,
): ContractErrorResponses {
  const errors = metadata.errors;
  if (typeof errors !== "object" || errors === null) return {};

  const catalog: ContractErrorResponses = {};
  for (const [key, definition] of Object.entries(errors)) {
    if (isContractErrorDefinition(definition)) {
      catalog[key] = definition;
    }
  }
  return catalog;
}

/**
 * Convert catalog error definitions into standard envelope response schemas.
 */
export function responsesFromErrors(
  errors: ContractErrorResponses,
): ContractResponses {
  const responses: ContractResponses = {};
  for (const error of Object.values(errors)) {
    responses[error.status] = STANDARD_ERROR_RESPONSE_SCHEMA;
  }
  return responses;
}

/**
 * Merge catalog errors already declared in metadata with new declarations.
 *
 * Later declarations win when the same catalog key is declared twice.
 */
export function mergeCatalogErrors(
  metadata: ContractMeta,
  errorDefs: ContractErrorResponses,
): ContractErrorResponses {
  return { ...catalogErrorsFromMeta(metadata), ...errorDefs };
}

function catalogErrorStatuses(metadata: ContractMeta): Set<string> {
  return new Set(
    Object.values(catalogErrorsFromMeta(metadata)).map((error) =>
      String(error.status),
    ),
  );
}

/**
 * Reject `.errors()` statuses that collide with custom response schemas.
 *
 * Statuses declared by earlier `.errors()` calls are allowed: catalog errors
 * share the standard error envelope, so multiple codes can map to one status.
 */
export function assertErrorsAvoidCustomResponseStatuses(
  metadata: ContractMeta,
  responses: ContractResponses,
  nextErrorResponses: ContractResponses,
): void {
  const existingErrorStatuses = catalogErrorStatuses(metadata);
  const customStatuses = new Set(
    Object.keys(responses).filter(
      (status) => !existingErrorStatuses.has(status),
    ),
  );
  const conflicts = Object.keys(nextErrorResponses).filter((status) =>
    customStatuses.has(status),
  );
  if (conflicts.length) {
    throw new Error(
      `.errors() cannot declare status ${conflicts.join(", ")} because a response schema for that status already exists. Use .responses() without .errors() when you need a custom error response body.`,
    );
  }
}

/**
 * Reject `.responses()` statuses already declared by catalog errors.
 */
export function assertResponsesAvoidCatalogErrorStatuses(
  metadata: ContractMeta,
  next: ContractResponses,
): void {
  const errorStatuses = catalogErrorStatuses(metadata);
  const conflicts = Object.keys(next).filter((status) =>
    errorStatuses.has(status),
  );
  if (conflicts.length) {
    throw new Error(
      `.responses() cannot declare status ${conflicts.join(", ")} because that status is already declared by .errors(). Use .responses() without .errors() when you need a custom error response body.`,
    );
  }
}
