import {
  assertErrorsAvoidCustomResponseStatuses,
  assertResponsesAvoidCatalogErrorStatuses,
  mergeCatalogErrors,
  responsesFromErrors,
} from "./catalog-errors.js";
import { ContractBuilder } from "./contract-builder.js";
import {
  assertValidContractDeprecation,
  type ContractDeprecationMeta,
} from "./lifecycle.js";
import { mergeContractMeta } from "./metadata.js";
import { parsePathTemplate } from "./path-template.js";
import type {
  ContractErrorResponses,
  ContractHeaderSchemas,
  ContractMeta,
  ContractResponses,
  HttpMethod,
  MergeContractMeta,
  MergedContractErrorResponses,
  OmitMetaKeys,
  ResponsesFromErrorDefinitions,
  StandardSchema,
} from "./types.js";
import { generateContractName } from "./utils.js";

type TrimLeadingSlash<T extends string> = T extends `/${infer Rest}`
  ? TrimLeadingSlash<Rest>
  : T;

type TrimTrailingSlash<T extends string> = T extends "/"
  ? ""
  : T extends `${infer Rest}/`
    ? TrimTrailingSlash<Rest>
    : T;

type NormalizedPrefix<T extends string> = T extends "" | "/"
  ? ""
  : TrimTrailingSlash<T>;

type NormalizedChildPath<T extends string> = TrimTrailingSlash<
  TrimLeadingSlash<T>
>;

type JoinPaths<TPrefix extends string, TPath extends string> = string extends
  | TPrefix
  | TPath
  ? string
  : NormalizedPrefix<TPrefix> extends ""
    ? TPath
    : NormalizedChildPath<TPath> extends ""
      ? NormalizedPrefix<TPrefix>
      : `${NormalizedPrefix<TPrefix>}/${NormalizedChildPath<TPath>}`;

function joinPathPrefix(prefix: string, path: string): string {
  const segments = [
    ...prefix.split("/").filter(Boolean),
    ...path.split("/").filter(Boolean),
  ];

  return `/${segments.join("/")}`;
}

/**
 * Feature-scoped factory for related HTTP contracts.
 *
 * Contract groups let a feature share a namespace, path prefix, headers,
 * responses, errors, and metadata across multiple endpoint contracts. The group
 * is immutable: every configuration method returns a new group.
 */
export class ContractGroup<
  TSharedResponses extends ContractResponses = Record<never, never>,
  TSharedMeta extends ContractMeta = ContractMeta,
  TSharedHeaders extends ContractHeaderSchemas = null,
  TPathPrefix extends string = "",
> {
  private readonly _namespace: string;
  private readonly _meta: TSharedMeta;
  private readonly _responses: TSharedResponses;
  private readonly _headers: TSharedHeaders;
  private readonly _pathPrefix: TPathPrefix;

  constructor(
    state: {
      namespace?: string;
      meta?: TSharedMeta;
      responses?: TSharedResponses;
      headers?: TSharedHeaders;
      pathPrefix?: TPathPrefix;
    } = {} as {
      namespace?: string;
      meta?: TSharedMeta;
      responses?: TSharedResponses;
      headers?: TSharedHeaders;
      pathPrefix?: TPathPrefix;
    },
  ) {
    this._namespace = state.namespace ?? "";
    this._meta = state.meta ?? ({} as TSharedMeta);
    this._responses = state.responses ?? ({} as TSharedResponses);
    this._headers = state.headers ?? (null as TSharedHeaders);
    this._pathPrefix = state.pathPrefix ?? ("" as TPathPrefix);
  }

  /**
   * Set the namespace for contracts created from this group.
   *
   * The namespace prefixes contract names, not paths. Use `prefix(...)` for
   * path composition.
   */
  namespace(
    ns: string,
  ): ContractGroup<TSharedResponses, TSharedMeta, TSharedHeaders, TPathPrefix> {
    return new ContractGroup({
      namespace: ns,
      meta: this._meta,
      responses: this._responses,
      headers: this._headers,
      pathPrefix: this._pathPrefix,
    });
  }

  /**
   * Add a path prefix to contracts created from this group.
   *
   * Prefixes compose immutably, so `prefix("/api").prefix("/v1")` produces
   * paths under `/api/v1`.
   */
  prefix<const TNewPrefix extends string>(
    pathPrefix: TNewPrefix,
  ): ContractGroup<
    TSharedResponses,
    TSharedMeta,
    TSharedHeaders,
    JoinPaths<TPathPrefix, TNewPrefix>
  > {
    parsePathTemplate(pathPrefix);
    const nextPrefix = joinPathPrefix(this._pathPrefix, pathPrefix);
    const normalizedPrefix = nextPrefix === "/" ? "" : nextPrefix;

    return new ContractGroup({
      namespace: this._namespace,
      meta: this._meta,
      responses: this._responses,
      headers: this._headers,
      pathPrefix: normalizedPrefix as JoinPaths<TPathPrefix, TNewPrefix>,
    });
  }

  /**
   * Merge shared metadata into contracts created from this group.
   */
  meta<TNewMeta extends ContractMeta>(
    meta: TNewMeta,
  ): ContractGroup<
    TSharedResponses,
    MergeContractMeta<TSharedMeta, TNewMeta>,
    TSharedHeaders,
    TPathPrefix
  > {
    return new ContractGroup({
      namespace: this._namespace,
      meta: mergeContractMeta(this._meta, meta),
      responses: this._responses,
      headers: this._headers,
      pathPrefix: this._pathPrefix,
    });
  }

  /**
   * Mark every contract created from this group as deprecated.
   */
  deprecated<const TDeprecation extends ContractDeprecationMeta>(
    deprecation: TDeprecation,
  ): ContractGroup<
    TSharedResponses,
    MergeContractMeta<TSharedMeta, { deprecation: TDeprecation }>,
    TSharedHeaders,
    TPathPrefix
  > {
    assertValidContractDeprecation(deprecation, this._namespace || "group");
    return new ContractGroup({
      namespace: this._namespace,
      meta: mergeContractMeta(this._meta, { deprecation }),
      responses: this._responses,
      headers: this._headers,
      pathPrefix: this._pathPrefix,
    });
  }

  /**
   * Add shared route-owned response schemas to contracts created from this group.
   *
   * Framework-owned responses, such as validation or auth hook failures, do not
   * need to be declared here.
   */
  responses<TNewResponses extends ContractResponses>(
    responseSchemas: TNewResponses,
  ): ContractGroup<
    Omit<TSharedResponses, keyof TNewResponses> & TNewResponses,
    TSharedMeta,
    TSharedHeaders,
    TPathPrefix
  > {
    assertResponsesAvoidCatalogErrorStatuses(this._meta, responseSchemas);
    return new ContractGroup({
      namespace: this._namespace,
      meta: this._meta,
      responses: { ...this._responses, ...responseSchemas } as unknown as Omit<
        TSharedResponses,
        keyof TNewResponses
      > &
        TNewResponses,
      headers: this._headers,
      pathPrefix: this._pathPrefix,
    });
  }

  /**
   * Declare shared route-owned application errors for contracts in this group.
   *
   * Catalog errors use Beignet's standard error envelope and remain separate
   * from framework-owned errors. Declarations merge with previously declared
   * group errors, and contracts created from the group merge these shared
   * errors with route-level `.errors()` declarations; later declarations win
   * when the same catalog key is declared twice.
   */
  errors<TErrorDefs extends ContractErrorResponses>(
    errorDefs: TErrorDefs,
  ): ContractGroup<
    Omit<TSharedResponses, keyof ResponsesFromErrorDefinitions<TErrorDefs>> &
      ResponsesFromErrorDefinitions<TErrorDefs>,
    OmitMetaKeys<TSharedMeta, "errors"> & {
      errors: MergedContractErrorResponses<TSharedMeta, TErrorDefs>;
    },
    TSharedHeaders,
    TPathPrefix
  > {
    const errorResponses = responsesFromErrors(errorDefs);
    assertErrorsAvoidCustomResponseStatuses(
      this._meta,
      this._responses,
      errorResponses,
    );
    return new ContractGroup({
      namespace: this._namespace,
      meta: {
        ...this._meta,
        errors: mergeCatalogErrors(this._meta, errorDefs),
      } as unknown as OmitMetaKeys<TSharedMeta, "errors"> & {
        errors: MergedContractErrorResponses<TSharedMeta, TErrorDefs>;
      },
      responses: { ...this._responses, ...errorResponses } as unknown as Omit<
        TSharedResponses,
        keyof ResponsesFromErrorDefinitions<TErrorDefs>
      > &
        ResponsesFromErrorDefinitions<TErrorDefs>,
      headers: this._headers,
      pathPrefix: this._pathPrefix,
    });
  }

  /**
   * Add a shared request header schema to contracts created from this group.
   */
  headers<TNewHeaders extends StandardSchema>(
    schema: TNewHeaders,
  ): ContractGroup<
    TSharedResponses,
    TSharedMeta,
    TSharedHeaders extends readonly StandardSchema[]
      ? readonly [...TSharedHeaders, TNewHeaders]
      : TSharedHeaders extends StandardSchema
        ? readonly [TSharedHeaders, TNewHeaders]
        : readonly [TNewHeaders],
    TPathPrefix
  > {
    const existingHeaders = this._headers
      ? Array.isArray(this._headers)
        ? this._headers
        : [this._headers]
      : [];

    return new ContractGroup({
      namespace: this._namespace,
      meta: this._meta,
      responses: this._responses,
      headers: [
        ...existingHeaders,
        schema,
      ] as unknown as TSharedHeaders extends readonly StandardSchema[]
        ? readonly [...TSharedHeaders, TNewHeaders]
        : TSharedHeaders extends StandardSchema
          ? readonly [TSharedHeaders, TNewHeaders]
          : readonly [TNewHeaders],
      pathPrefix: this._pathPrefix,
    });
  }

  /**
   * Create a GET contract under this group.
   */
  get<const TPath extends string>(
    path: TPath,
    name?: string,
  ): ContractBuilder<
    "GET",
    null,
    null,
    null,
    TSharedHeaders,
    TSharedResponses,
    TSharedMeta,
    JoinPaths<TPathPrefix, TPath>
  > {
    return this.createBuilder("GET", path, name);
  }

  /**
   * Create a POST contract under this group.
   */
  post<const TPath extends string>(
    path: TPath,
    name?: string,
  ): ContractBuilder<
    "POST",
    null,
    null,
    null,
    TSharedHeaders,
    TSharedResponses,
    TSharedMeta,
    JoinPaths<TPathPrefix, TPath>
  > {
    return this.createBuilder("POST", path, name);
  }

  /**
   * Create a PUT contract under this group.
   */
  put<const TPath extends string>(
    path: TPath,
    name?: string,
  ): ContractBuilder<
    "PUT",
    null,
    null,
    null,
    TSharedHeaders,
    TSharedResponses,
    TSharedMeta,
    JoinPaths<TPathPrefix, TPath>
  > {
    return this.createBuilder("PUT", path, name);
  }

  /**
   * Create a PATCH contract under this group.
   */
  patch<const TPath extends string>(
    path: TPath,
    name?: string,
  ): ContractBuilder<
    "PATCH",
    null,
    null,
    null,
    TSharedHeaders,
    TSharedResponses,
    TSharedMeta,
    JoinPaths<TPathPrefix, TPath>
  > {
    return this.createBuilder("PATCH", path, name);
  }

  /**
   * Create a DELETE contract under this group.
   */
  delete<const TPath extends string>(
    path: TPath,
    name?: string,
  ): ContractBuilder<
    "DELETE",
    null,
    null,
    null,
    TSharedHeaders,
    TSharedResponses,
    TSharedMeta,
    JoinPaths<TPathPrefix, TPath>
  > {
    return this.createBuilder("DELETE", path, name);
  }

  /**
   * Create a HEAD contract under this group.
   */
  head<const TPath extends string>(
    path: TPath,
    name?: string,
  ): ContractBuilder<
    "HEAD",
    null,
    null,
    null,
    TSharedHeaders,
    TSharedResponses,
    TSharedMeta,
    JoinPaths<TPathPrefix, TPath>
  > {
    return this.createBuilder("HEAD", path, name);
  }

  /**
   * Create an OPTIONS contract under this group.
   */
  options<const TPath extends string>(
    path: TPath,
    name?: string,
  ): ContractBuilder<
    "OPTIONS",
    null,
    null,
    null,
    TSharedHeaders,
    TSharedResponses,
    TSharedMeta,
    JoinPaths<TPathPrefix, TPath>
  > {
    return this.createBuilder("OPTIONS", path, name);
  }

  /**
   * Internal helper to create a contract builder with shared config
   */
  private createBuilder<TMethod extends HttpMethod, const TPath extends string>(
    method: TMethod,
    path: TPath,
    name?: string,
  ): ContractBuilder<
    TMethod,
    null,
    null,
    null,
    TSharedHeaders,
    TSharedResponses,
    TSharedMeta,
    JoinPaths<TPathPrefix, TPath>
  > {
    parsePathTemplate(path);
    const fullPath = (
      this._pathPrefix ? joinPathPrefix(this._pathPrefix, path) : path
    ) as JoinPaths<TPathPrefix, TPath>;
    parsePathTemplate(fullPath);
    const contractName = name || generateContractName(method, fullPath);
    const fullName = this._namespace
      ? `${this._namespace}.${contractName}`
      : contractName;

    return new ContractBuilder({
      kind: "http",
      name: fullName,
      namespace: this._namespace || undefined,
      localName: contractName,
      method,
      path: fullPath,
      pathParams: null,
      query: null,
      headers: this._headers,
      body: null,
      responses: { ...this._responses } as unknown as TSharedResponses,
      metadata: { ...this._meta } as unknown as TSharedMeta,
    });
  }
}

/**
 * Create a new feature contract group.
 *
 * Start here for most feature HTTP surfaces, then add a namespace and path
 * prefix before defining individual contracts.
 *
 * @example
 * ```ts
 * const todos = defineContractGroup()
 *   .namespace("todos")
 *   .prefix("/api/todos")
 *   .meta({ auth: "required" })
 *   .responses({
 *     401: z.object({ message: z.literal("Unauthorized") }),
 *   });
 *
 * const getTodo = todos.get("/:id")...
 * ```
 *
 * @returns An empty immutable contract group.
 */
export function defineContractGroup(): ContractGroup<
  Record<never, never>,
  ContractMeta,
  null,
  ""
> {
  return new ContractGroup();
}
