import type { StandardSchemaV1 } from "@standard-schema/spec";
import {
  assertErrorsAvoidCustomResponseStatuses,
  assertResponsesAvoidCatalogErrorStatuses,
  mergeCatalogErrors,
  responsesFromErrors,
} from "./catalog-errors.js";
import { assertContractQueryTransport } from "./contract-like.js";
import {
  assertValidContractDeprecation,
  assertValidContractLifecycle,
  type ContractDeprecationMeta,
} from "./lifecycle.js";
import { mergeContractMeta } from "./metadata.js";
import type { OpenAPIOperationMeta } from "./openapi-meta.js";
import { parsePathTemplate } from "./path-template.js";
import type {
  QueryTransport,
  QueryTransportCompatibility,
} from "./query-transport.js";
import type {
  BodyHttpMethod,
  ContractErrorResponses,
  ContractHeaderSchemas,
  ContractMeta,
  ContractResponses,
  HttpContractConfig,
  HttpMethod,
  MergeContractMeta,
  MergedContractErrorResponses,
  OmitMetaKeys,
  ResponsesFromErrorDefinitions,
  StandardSchema,
} from "./types.js";
import { methodSupportsRequestBody } from "./types.js";
import { generateContractName } from "./utils.js";

/**
 * Fluent builder for one HTTP contract.
 *
 * A contract describes the transport boundary for one endpoint: method, path,
 * request schemas, response schemas, metadata, and route-owned catalog errors.
 * Builder methods are immutable; each call returns a new builder with narrower
 * types.
 */
export class ContractBuilder<
  TMethod extends HttpMethod,
  TPathParams extends StandardSchema | null,
  TQuery extends StandardSchema | null,
  TBody extends StandardSchema | null,
  THeaders extends ContractHeaderSchemas,
  TResponses extends ContractResponses,
  TMeta extends ContractMeta,
  TPath extends string = string,
> {
  readonly kind: "http";
  readonly name: string;
  readonly namespace?: string;
  readonly localName: string;
  readonly method: TMethod;
  private readonly _path: TPath;
  private readonly _pathParams: TPathParams;
  private readonly _query: TQuery;
  private readonly _queryTransport: QueryTransport | null;
  private readonly _body: TBody;
  private readonly _headers: THeaders;
  private readonly _responses: TResponses;
  private readonly _meta: TMeta;

  constructor(
    config: HttpContractConfig<
      TMethod,
      TPathParams,
      TQuery,
      TBody,
      TResponses,
      TMeta,
      TPath,
      THeaders
    >,
  ) {
    assertValidContractLifecycle(config);
    assertContractQueryTransport(config);
    if (config.body && !methodSupportsRequestBody(config.method)) {
      throw new Error(
        `Request bodies are not supported for ${config.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`,
      );
    }
    this.kind = config.kind;
    this.name = config.name;
    this.namespace = config.namespace;
    this.localName = config.localName ?? config.name;
    this.method = config.method;
    this._path = config.path;
    this._pathParams = config.pathParams;
    this._query = config.query;
    this._queryTransport = config.queryTransport ?? null;
    this._body = config.body;
    this._headers = config.headers ?? (null as THeaders);
    this._responses = config.responses;
    this._meta = config.metadata;
  }

  /**
   * Request and response schemas attached to this contract.
   *
   * Server adapters use these for validation. Client and frontend integrations
   * use them for local validation and type inference.
   */
  get schema() {
    return {
      pathParams: this._pathParams,
      query: this._query,
      headers: this._headers,
      body: this._body,
      responses: this._responses,
    };
  }

  /**
   * Response schemas keyed by HTTP status code.
   */
  get responseSchemas(): TResponses {
    return this._responses;
  }

  /**
   * URL path template for this contract.
   */
  get path(): TPath {
    return this._path;
  }

  /**
   * Metadata consumed by hooks, OpenAPI generation, and app conventions.
   */
  get metadata(): TMeta {
    return this._meta;
  }

  /**
   * Plain contract config consumed by framework internals and integrations.
   */
  get config(): HttpContractConfig<
    TMethod,
    TPathParams,
    TQuery,
    TBody,
    TResponses,
    TMeta,
    TPath,
    THeaders
  > {
    return {
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: this._pathParams,
      query: this._query,
      queryTransport: this._queryTransport,
      headers: this._headers,
      body: this._body,
      responses: this._responses,
      metadata: this._meta,
    };
  }

  /**
   * Attach a schema for dynamic path parameters.
   *
   * The schema validates parameters parsed from path templates such as
   * `/posts/:id` or `/posts/[id]`.
   */
  pathParams<TNewPathParams extends StandardSchemaV1>(
    schema: TNewPathParams,
  ): ContractBuilder<
    TMethod,
    TNewPathParams,
    TQuery,
    TBody,
    THeaders,
    TResponses,
    TMeta,
    TPath
  > {
    return new ContractBuilder({
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: schema as unknown as TNewPathParams,
      query: this._query,
      queryTransport: this._queryTransport,
      headers: this._headers,
      body: this._body,
      responses: this._responses,
      metadata: this._meta,
    });
  }

  /**
   * Attach a query schema and the deterministic URL transport for its input.
   *
   * The schema owns validation, defaults, and transforms. The transport owns
   * client encoding, server decoding, and OpenAPI serialization.
   */
  query<
    TNewQuery extends StandardSchemaV1,
    const TTransport extends QueryTransport,
  >(
    schema: TNewQuery,
    transport: TTransport &
      QueryTransportCompatibility<
        StandardSchemaV1.InferInput<TNewQuery>,
        TTransport
      >,
  ): ContractBuilder<
    TMethod,
    TPathParams,
    TNewQuery,
    TBody,
    THeaders,
    TResponses,
    TMeta,
    TPath
  > {
    return new ContractBuilder({
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: this._pathParams,
      query: schema as unknown as TNewQuery,
      queryTransport: transport,
      headers: this._headers,
      body: this._body,
      responses: this._responses,
      metadata: this._meta,
    });
  }

  /**
   * Attach a schema for a JSON request body.
   *
   * This method is only available on POST, PUT, and PATCH contracts.
   */
  body<TNewBody extends StandardSchemaV1>(
    this: ContractBuilder<
      BodyHttpMethod,
      StandardSchema | null,
      StandardSchema | null,
      StandardSchema | null,
      ContractHeaderSchemas,
      ContractResponses,
      ContractMeta,
      TPath
    >,
    schema: TNewBody,
  ): ContractBuilder<
    TMethod,
    TPathParams,
    TQuery,
    TNewBody,
    THeaders,
    TResponses,
    TMeta,
    TPath
  > {
    const self = this as unknown as ContractBuilder<
      TMethod,
      TPathParams,
      TQuery,
      TBody,
      THeaders,
      TResponses,
      TMeta,
      TPath
    >;
    return new ContractBuilder({
      kind: self.kind,
      name: self.name,
      namespace: self.namespace,
      localName: self.localName,
      method: self.method,
      path: self._path,
      pathParams: self._pathParams,
      query: self._query,
      queryTransport: self._queryTransport,
      headers: self._headers,
      body: schema as unknown as TNewBody,
      responses: self._responses,
      metadata: self._meta,
    });
  }

  /**
   * Attach a request header schema.
   *
   * Multiple schemas are evaluated in declaration order and their parsed
   * outputs are merged. This lets a contract inherit shared group headers and
   * still declare route-specific headers.
   */
  headers<TNewHeaders extends StandardSchemaV1>(
    schema: TNewHeaders,
  ): ContractBuilder<
    TMethod,
    TPathParams,
    TQuery,
    TBody,
    THeaders extends readonly StandardSchema[]
      ? readonly [...THeaders, TNewHeaders]
      : THeaders extends StandardSchema
        ? readonly [THeaders, TNewHeaders]
        : readonly [TNewHeaders],
    TResponses,
    TMeta,
    TPath
  > {
    const existingHeaders = this._headers
      ? Array.isArray(this._headers)
        ? this._headers
        : [this._headers]
      : [];

    return new ContractBuilder({
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: this._pathParams,
      query: this._query,
      queryTransport: this._queryTransport,
      headers: [
        ...existingHeaders,
        schema,
      ] as unknown as THeaders extends readonly StandardSchema[]
        ? readonly [...THeaders, TNewHeaders]
        : THeaders extends StandardSchema
          ? readonly [THeaders, TNewHeaders]
          : readonly [TNewHeaders],
      body: this._body,
      responses: this._responses,
      metadata: this._meta,
    });
  }

  /**
   * Add or replace route-owned response schemas by status code.
   *
   * These schemas describe business responses returned by route handlers.
   * Framework-owned errors such as request validation failures do not need to be
   * declared here.
   *
   * Use `null` for void/empty responses such as 204 No Content.
   */
  responses<TNewResponses extends ContractResponses>(
    responseSchemas: TNewResponses,
  ): ContractBuilder<
    TMethod,
    TPathParams,
    TQuery,
    TBody,
    THeaders,
    Omit<TResponses, keyof TNewResponses> & TNewResponses,
    TMeta,
    TPath
  > {
    assertResponsesAvoidCatalogErrorStatuses(this._meta, responseSchemas);
    return new ContractBuilder({
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: this._pathParams,
      query: this._query,
      queryTransport: this._queryTransport,
      headers: this._headers,
      body: this._body,
      responses: {
        ...this._responses,
        ...responseSchemas,
      } as unknown as Omit<TResponses, keyof TNewResponses> & TNewResponses,
      metadata: this._meta,
    });
  }

  /**
   * Declare route-owned application errors from an error catalog.
   *
   * Catalog errors use Beignet's standard error response envelope and remain
   * distinguishable from framework-owned errors. Declarations merge with
   * previously declared catalog errors, including shared group errors; later
   * declarations win when the same catalog key is declared twice. Use
   * `.responses()` when a route needs a custom error response body instead of
   * catalog semantics.
   */
  errors<TErrorDefs extends ContractErrorResponses>(
    errorDefs: TErrorDefs,
  ): ContractBuilder<
    TMethod,
    TPathParams,
    TQuery,
    TBody,
    THeaders,
    Omit<TResponses, keyof ResponsesFromErrorDefinitions<TErrorDefs>> &
      ResponsesFromErrorDefinitions<TErrorDefs>,
    OmitMetaKeys<TMeta, "errors"> & {
      errors: MergedContractErrorResponses<TMeta, TErrorDefs>;
    },
    TPath
  > {
    const errorResponses = responsesFromErrors(errorDefs);
    assertErrorsAvoidCustomResponseStatuses(
      this._meta,
      this._responses,
      errorResponses,
    );
    return new ContractBuilder({
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: this._pathParams,
      query: this._query,
      queryTransport: this._queryTransport,
      headers: this._headers,
      body: this._body,
      responses: {
        ...this._responses,
        ...errorResponses,
      } as unknown as Omit<
        TResponses,
        keyof ResponsesFromErrorDefinitions<TErrorDefs>
      > &
        ResponsesFromErrorDefinitions<TErrorDefs>,
      metadata: {
        ...this._meta,
        errors: mergeCatalogErrors(this._meta, errorDefs),
      } as unknown as OmitMetaKeys<TMeta, "errors"> & {
        errors: MergedContractErrorResponses<TMeta, TErrorDefs>;
      },
    });
  }

  /**
   * Merge metadata into this contract.
   *
   * Hooks and tooling can read metadata for concerns such as auth, rate limits,
   * idempotency, OpenAPI, or app-specific conventions.
   */
  meta<TNewMeta extends ContractMeta>(
    newMeta: TNewMeta,
  ): ContractBuilder<
    TMethod,
    TPathParams,
    TQuery,
    TBody,
    THeaders,
    TResponses,
    MergeContractMeta<TMeta, TNewMeta>,
    TPath
  > {
    return new ContractBuilder({
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: this._pathParams,
      query: this._query,
      queryTransport: this._queryTransport,
      headers: this._headers,
      body: this._body,
      responses: this._responses,
      metadata: mergeContractMeta(this._meta, newMeta),
    });
  }

  /**
   * Mark this contract as deprecated for external clients.
   */
  deprecated<const TDeprecation extends ContractDeprecationMeta>(
    deprecation: TDeprecation,
  ): ContractBuilder<
    TMethod,
    TPathParams,
    TQuery,
    TBody,
    THeaders,
    TResponses,
    MergeContractMeta<TMeta, { deprecation: TDeprecation }>,
    TPath
  > {
    assertValidContractDeprecation(deprecation, this.name);
    return new ContractBuilder({
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: this._pathParams,
      query: this._query,
      queryTransport: this._queryTransport,
      headers: this._headers,
      body: this._body,
      responses: this._responses,
      metadata: mergeContractMeta(this._meta, { deprecation }),
    });
  }

  /**
   * Merge OpenAPI operation metadata into this contract.
   */
  openapi<TPatch extends Partial<OpenAPIOperationMeta>>(
    patch: TPatch,
  ): ContractBuilder<
    TMethod,
    TPathParams,
    TQuery,
    TBody,
    THeaders,
    TResponses,
    MergeContractMeta<TMeta, { openapi: TPatch }>,
    TPath
  > {
    return new ContractBuilder({
      kind: this.kind,
      name: this.name,
      namespace: this.namespace,
      localName: this.localName,
      method: this.method,
      path: this._path,
      pathParams: this._pathParams,
      query: this._query,
      queryTransport: this._queryTransport,
      headers: this._headers,
      body: this._body,
      responses: this._responses,
      metadata: mergeContractMeta(this._meta, { openapi: patch }),
    });
  }
}

/**
 * Options for creating one contract with `defineContract(...)`.
 */
export type DefineContractOptions<
  TMethod extends HttpMethod = HttpMethod,
  TPath extends string = string,
> = {
  /** HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) */
  method: TMethod;
  /** URL path template (e.g., "/api/users/:id") */
  path: TPath;
  /** Optional contract name (auto-generated from method + path if not provided) */
  name?: string;
};

/**
 * Create a new HTTP contract builder.
 *
 * Most apps prefer `defineContractGroup().namespace(...).prefix(...)` for
 * related feature contracts. Use this lower-level factory when a standalone
 * contract is clearer.
 *
 * @example
 * ```ts
 * const getTodo = defineContract({
 *   method: "GET",
 *   path: "/api/todos/:id",
 * })
 *   .pathParams(z.object({ id: z.string() }))
 *   .responses({ 200: TodoSchema });
 * ```
 *
 * @param options - HTTP method, path template, and optional contract name.
 * @returns A fluent contract builder.
 */
export function defineContract<
  TMethod extends HttpMethod,
  const TPath extends string,
>(
  options: DefineContractOptions<TMethod, TPath>,
): ContractBuilder<
  TMethod,
  null,
  null,
  null,
  null,
  Record<never, never>,
  ContractMeta,
  TPath
> {
  parsePathTemplate(options.path);
  const name =
    options.name || generateContractName(options.method, options.path);
  return new ContractBuilder({
    kind: "http",
    name,
    localName: name,
    method: options.method,
    path: options.path,
    pathParams: null,
    query: null,
    queryTransport: null,
    headers: null,
    body: null,
    responses: {},
    metadata: {},
  });
}
