import { Effect } from "effect";
import { requestFromService, runLayerResult } from "../effect";
import type { FMODataErrorType } from "../errors";
import { BatchTruncatedError } from "../errors";
import type { FMODataLayer, ODataConfig } from "../services";
import type {
  BatchItemResult,
  BatchResult,
  ExecutableBuilder,
  ExecuteMethodOptions,
  ExecuteOptions,
  Result,
} from "../types";
import { formatBatchRequestFromNative, type ParsedBatchResponse, parseBatchResponse } from "./batch-request";
import { normalizeDatabasePath } from "./database-name";
import { createClientRuntime } from "./runtime";

/**
 * Helper type to extract result types from a tuple of ExecutableBuilders.
 * Uses a mapped type which TypeScript 4.1+ can handle for tuples.
 */
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type
type ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {
  [K in keyof T]: T[K] extends {
    processResponse(response: Response, options?: ExecuteOptions): Promise<Result<infer U>>;
  }
    ? U
    : never;
};

/**
 * Converts a ParsedBatchResponse to a native Response object
 * @param parsed - The parsed batch response
 * @returns A native Response object
 */
function parsedToResponse(parsed: ParsedBatchResponse): Response {
  const headers = new Headers(parsed.headers);

  // Handle null body
  if (parsed.body === null || parsed.body === undefined) {
    return new Response(null, {
      status: parsed.status,
      statusText: parsed.statusText,
      headers,
    });
  }

  // Convert body to string if it's not already
  const bodyString = typeof parsed.body === "string" ? parsed.body : JSON.stringify(parsed.body);

  // Handle 204 No Content status - it cannot have a body per HTTP spec
  // If FileMaker returns 204 with a body, treat it as 200
  let status = parsed.status;
  if (status === 204 && bodyString && bodyString.trim() !== "") {
    status = 200;
  }

  return new Response(status === 204 ? null : bodyString, {
    status,
    statusText: parsed.statusText,
    headers,
  });
}

/**
 * Builder for batch operations that allows multiple queries to be executed together
 * in a single transactional request.
 *
 * Note: BatchBuilder does not implement ExecutableBuilder because execute() returns
 * BatchResult instead of Result, which is a different return type structure.
 */
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type
export class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]> {
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type
  private readonly builders: ExecutableBuilder<any>[];
  private readonly layer: FMODataLayer;
  private readonly config: ODataConfig;

  constructor(builders: Builders, layer: FMODataLayer) {
    // Convert readonly tuple to mutable array for dynamic additions
    this.builders = [...builders];
    const runtime = createClientRuntime(layer);
    this.layer = runtime.layer;
    this.config = runtime.config;
  }

  /**
   * Add a request to the batch dynamically.
   * This allows building up batch operations programmatically.
   *
   * @param builder - An executable builder to add to the batch
   * @returns A BatchBuilder typed with the appended request result
   * @example
   * ```ts
   * const batch = db.batch([]);
   * batch.addRequest(db.from('contacts').list());
   * batch.addRequest(db.from('users').list());
   * const result = await batch.execute();
   * ```
   */
  addRequest<T>(builder: ExecutableBuilder<T>): BatchBuilder<[...Builders, ExecutableBuilder<T>]> {
    this.builders.push(builder);
    return this as unknown as BatchBuilder<[...Builders, ExecutableBuilder<T>]>;
  }

  /**
   * Get the request configuration for this batch operation.
   * This is used internally by the execution system.
   */
  // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value
  getRequestConfig(): { method: string; url: string; body?: any } {
    return {
      method: "POST",
      url: `/${this.config.databaseName}/$batch`,
      body: undefined, // Body is constructed in execute()
    };
  }

  toRequest(baseUrl: string, _options?: ExecuteOptions): Request {
    const fullUrl = `${baseUrl}${normalizeDatabasePath(`/${this.config.databaseName}/$batch`, {
      normalizeDatabaseName: _options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName,
    })}`;
    return new Request(fullUrl, {
      method: "POST",
      headers: {
        "Content-Type": "multipart/mixed",
        "OData-Version": "4.0",
      },
    });
  }

  // biome-ignore lint/suspicious/noExplicitAny: Generic return type for interface compliance
  processResponse(_response: Response, _options?: ExecuteOptions): Promise<Result<any>> {
    return Promise.resolve({
      data: undefined,
      error: {
        name: "NotImplementedError",
        message: "Batch operations handle response processing internally",
        timestamp: new Date(),
        // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
      } as any,
    });
  }

  /**
   * Creates a failed BatchResult where all operations are marked as failed with the given error.
   */
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type
  private failAllResults(error: any): BatchResult<ExtractTupleTypes<Builders>> {
    const errorCount = this.builders.length;
    // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type
    const results: BatchItemResult<any>[] = this.builders.map(() => ({
      data: undefined,
      error,
      status: 0,
    }));
    return {
      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
      results: results as any,
      successCount: 0,
      errorCount,
      truncated: false,
      firstErrorIndex: errorCount > 0 ? 0 : null,
    };
  }

  /**
   * Execute the batch operation.
   *
   * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)
   * @returns A BatchResult containing individual results for each operation
   */
  async execute<EO extends ExecuteOptions>(
    options?: ExecuteMethodOptions<EO>,
  ): Promise<BatchResult<ExtractTupleTypes<Builders>>> {
    const baseUrl = this.config.baseUrl;
    if (!baseUrl) {
      return this.failAllResults({
        name: "ConfigurationError",
        message: "Base URL not available in ODataConfig",
        timestamp: new Date(),
      });
    }

    const pipeline = Effect.gen(this, function* () {
      // Step 1: Convert builders to Request objects and format batch
      const requests: Request[] = this.builders.map((builder) => builder.toRequest(baseUrl, options));
      const { body, boundary } = yield* Effect.tryPromise({
        try: () => formatBatchRequestFromNative(requests, baseUrl),
        catch: (e) => e as FMODataErrorType,
      });

      // Step 2: Execute the batch HTTP request via DI
      const responseData = yield* requestFromService<string>(`/${this.config.databaseName}/$batch`, {
        ...options,
        method: "POST",
        headers: {
          ...options?.headers,
          "Content-Type": `multipart/mixed; boundary=${boundary}`,
          "OData-Version": "4.0",
        },
        body,
      });

      // Step 3: Parse multipart response
      const firstLine = responseData.split("\r\n")[0] || responseData.split("\n")[0] || "";
      const actualBoundary = firstLine.startsWith("--") ? firstLine.substring(2) : boundary;
      const contentTypeHeader = `multipart/mixed; boundary=${actualBoundary}`;
      const parsedResponses = parseBatchResponse(responseData, contentTypeHeader);

      // Step 4: Process each response using the corresponding builder
      // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type
      const results: BatchItemResult<any>[] = [];
      let successCount = 0;
      let errorCount = 0;
      let firstErrorIndex: number | null = null;
      const truncated = parsedResponses.length < this.builders.length;

      for (let i = 0; i < this.builders.length; i++) {
        const builder = this.builders[i];
        const parsed = parsedResponses[i];

        if (!parsed) {
          const failedAtIndex = firstErrorIndex ?? i;
          results.push({
            data: undefined,
            error: new BatchTruncatedError(i, failedAtIndex),
            status: 0,
          });
          errorCount++;
          continue;
        }

        if (!builder) {
          results.push({
            data: undefined,
            error: {
              name: "BatchError",
              message: `Builder at index ${i} is undefined`,
              timestamp: new Date(),
              // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
            } as any,
            status: parsed.status,
          });
          errorCount++;
          if (firstErrorIndex === null) {
            firstErrorIndex = i;
          }
          continue;
        }

        const nativeResponse = parsedToResponse(parsed);
        const result = yield* Effect.tryPromise({
          try: () => builder.processResponse(nativeResponse, options),
          catch: (e) => e as FMODataErrorType,
        });

        if (result.error) {
          results.push({ data: undefined, error: result.error, status: parsed.status });
          errorCount++;
          if (firstErrorIndex === null) {
            firstErrorIndex = i;
          }
        } else {
          results.push({ data: result.data, error: undefined, status: parsed.status });
          successCount++;
        }
      }

      return {
        // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
        results: results as any,
        successCount,
        errorCount,
        truncated,
        firstErrorIndex,
      } as BatchResult<ExtractTupleTypes<Builders>>;
    });

    // For batch, errors at the transport level fail all operations
    const result = await runLayerResult(this.layer, pipeline, "fmodata.batch");
    if (result.error) {
      return this.failAllResults(result.error);
    }
    return result.data;
  }
}
