import type { HttpContractConfig } from "./types.js";

/**
 * Lifecycle metadata for an HTTP contract that external clients should stop
 * using.
 */
export type ContractDeprecationMeta = {
  /** UTC ISO 8601 timestamp when the contract became deprecated. */
  since: string;
  /** Optional human-readable explanation. */
  reason?: string;
  /** UTC ISO 8601 timestamp after which the contract may stop being served. */
  sunset?: string;
  /** URI reference for the preferred replacement operation. */
  replacement?: string;
  /** Absolute HTTP(S) URL with migration or deprecation documentation. */
  documentation?: string;
};

/** Stable code identifying invalid lifecycle or operation metadata. */
export type ContractLifecycleFindingCode =
  | "INVALID_DEPRECATION_SINCE"
  | "INVALID_DEPRECATION_SUNSET"
  | "DEPRECATION_SUNSET_BEFORE_SINCE"
  | "INVALID_DEPRECATION_REASON"
  | "INVALID_DEPRECATION_REPLACEMENT"
  | "INVALID_DEPRECATION_DOCUMENTATION"
  | "INVALID_OPERATION_ID";

/** Error raised when contract lifecycle metadata is malformed. */
export class ContractLifecycleError extends Error {
  /** Stable machine-readable finding code. */
  readonly code: ContractLifecycleFindingCode;
  /** Name of the invalid contract, or the group label during group setup. */
  readonly contract: string;

  constructor(args: {
    code: ContractLifecycleFindingCode;
    contract: string;
    message: string;
  }) {
    super(args.message);
    this.name = "ContractLifecycleError";
    this.code = args.code;
    this.contract = args.contract;
  }
}

const ISO_8601_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
const INVALID_URI_REFERENCE_CHARACTERS = /[\s<>"\\]/;
const INVALID_PERCENT_ENCODING = /%(?![0-9A-Fa-f]{2})/;

function parseUtcTimestamp(value: string): number | undefined {
  if (!ISO_8601_UTC.test(value)) return undefined;
  const timestamp = Date.parse(value);
  if (!Number.isFinite(timestamp)) return undefined;

  const canonical = new Date(timestamp).toISOString();
  if (value !== canonical && value !== canonical.replace(".000Z", "Z")) {
    return undefined;
  }

  return timestamp;
}

function assertNonEmptyOptionalString(args: {
  value: unknown;
  field: "reason" | "replacement";
  code: ContractLifecycleFindingCode;
  contract: string;
}): string | undefined {
  if (args.value === undefined) return undefined;
  if (typeof args.value !== "string" || args.value.trim().length === 0) {
    throw new ContractLifecycleError({
      code: args.code,
      contract: args.contract,
      message: `Contract "${args.contract}" deprecation ${args.field} must be a non-empty string.`,
    });
  }
  return args.value;
}

/** Validate deprecation metadata supplied by builders or raw contract configs. */
export function assertValidContractDeprecation(
  deprecation: unknown,
  contract: string,
): asserts deprecation is ContractDeprecationMeta {
  if (typeof deprecation !== "object" || deprecation === null) {
    throw new ContractLifecycleError({
      code: "INVALID_DEPRECATION_SINCE",
      contract,
      message: `Contract "${contract}" deprecation metadata must include a valid "since" timestamp.`,
    });
  }

  const metadata = deprecation as Record<string, unknown>;
  const since =
    typeof metadata.since === "string"
      ? parseUtcTimestamp(metadata.since)
      : undefined;
  if (since === undefined) {
    throw new ContractLifecycleError({
      code: "INVALID_DEPRECATION_SINCE",
      contract,
      message: `Contract "${contract}" deprecation "since" must be a valid UTC ISO 8601 timestamp such as 2026-07-11T00:00:00Z.`,
    });
  }

  let sunset: number | undefined;
  if (metadata.sunset !== undefined) {
    sunset =
      typeof metadata.sunset === "string"
        ? parseUtcTimestamp(metadata.sunset)
        : undefined;
    if (sunset === undefined) {
      throw new ContractLifecycleError({
        code: "INVALID_DEPRECATION_SUNSET",
        contract,
        message: `Contract "${contract}" deprecation "sunset" must be a valid UTC ISO 8601 timestamp.`,
      });
    }
    if (sunset < since) {
      throw new ContractLifecycleError({
        code: "DEPRECATION_SUNSET_BEFORE_SINCE",
        contract,
        message: `Contract "${contract}" deprecation "sunset" must not be earlier than "since".`,
      });
    }
  }

  assertNonEmptyOptionalString({
    value: metadata.reason,
    field: "reason",
    code: "INVALID_DEPRECATION_REASON",
    contract,
  });
  const replacement = assertNonEmptyOptionalString({
    value: metadata.replacement,
    field: "replacement",
    code: "INVALID_DEPRECATION_REPLACEMENT",
    contract,
  });
  let replacementIsValid = true;
  if (replacement) {
    try {
      new URL(replacement, "https://beignet.invalid");
    } catch {
      replacementIsValid = false;
    }
  }
  if (
    replacement &&
    (!replacementIsValid ||
      INVALID_URI_REFERENCE_CHARACTERS.test(replacement) ||
      INVALID_PERCENT_ENCODING.test(replacement))
  ) {
    throw new ContractLifecycleError({
      code: "INVALID_DEPRECATION_REPLACEMENT",
      contract,
      message: `Contract "${contract}" deprecation "replacement" must be a valid URI reference without whitespace.`,
    });
  }

  if (metadata.documentation !== undefined) {
    let documentation: URL | undefined;
    if (typeof metadata.documentation === "string") {
      try {
        documentation = new URL(metadata.documentation);
      } catch {
        documentation = undefined;
      }
    }
    if (
      !documentation ||
      (documentation.protocol !== "http:" &&
        documentation.protocol !== "https:") ||
      INVALID_URI_REFERENCE_CHARACTERS.test(metadata.documentation as string)
    ) {
      throw new ContractLifecycleError({
        code: "INVALID_DEPRECATION_DOCUMENTATION",
        contract,
        message: `Contract "${contract}" deprecation "documentation" must be an absolute HTTP(S) URL.`,
      });
    }
  }
}

/** Return and validate the operation ID used by OpenAPI and route registries. */
export function getContractOperationId(
  contract: Pick<HttpContractConfig, "name" | "metadata">,
): string {
  const operationId = contract.metadata.openapi?.operationId ?? contract.name;
  if (
    typeof operationId !== "string" ||
    operationId.trim().length === 0 ||
    operationId !== operationId.trim()
  ) {
    throw new ContractLifecycleError({
      code: "INVALID_OPERATION_ID",
      contract: contract.name,
      message: `Contract "${contract.name}" operationId must be a non-empty string without surrounding whitespace.`,
    });
  }
  return operationId;
}

/** Validate lifecycle metadata on a complete contract definition. */
export function assertValidContractLifecycle(
  contract: Pick<HttpContractConfig, "name" | "metadata">,
): void {
  getContractOperationId(contract);
  const deprecation = contract.metadata.deprecation;
  if (deprecation !== undefined) {
    assertValidContractDeprecation(deprecation, contract.name);
  }
}

/** Build standards-based HTTP response headers for a deprecated contract. */
export function contractLifecycleResponseHeaders(
  contract: Pick<HttpContractConfig, "name" | "metadata">,
): Record<string, string> {
  const deprecation = contract.metadata.deprecation;
  if (deprecation === undefined) return {};
  assertValidContractDeprecation(deprecation, contract.name);

  const headers: Record<string, string> = {
    Deprecation: `@${Math.floor(Date.parse(deprecation.since) / 1000)}`,
  };
  if (deprecation.sunset) {
    headers.Sunset = new Date(deprecation.sunset).toUTCString();
  }
  if (deprecation.documentation) {
    headers.Link = `<${deprecation.documentation}>; rel="deprecation"`;
  }
  return headers;
}
