import * as plugins from './plugins.js';
import * as types from './types.js';
import { CoreResponse } from './response.js';
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
import { MultipartFormData } from '../core_base/multipartformdata.js';

// Keep-alive agents for connection pooling
const httpAgent = new plugins.agentkeepalive.HttpAgent({
  keepAlive: true,
  maxFreeSockets: 10,
  maxSockets: 100,
  maxTotalSockets: 1000,
});

const httpAgentKeepAliveFalse = new plugins.agentkeepalive.HttpAgent({
  keepAlive: false,
});

const httpsAgent = new plugins.agentkeepalive.HttpsAgent({
  keepAlive: true,
  maxFreeSockets: 10,
  maxSockets: 100,
  maxTotalSockets: 1000,
});

const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
  keepAlive: false,
});

/**
 * Node.js implementation of Core Request class that handles all HTTP/HTTPS requests
 */
export class CoreRequest extends AbstractCoreRequest<
  types.ICoreRequestOptions,
  CoreResponse
> {
  private requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null;

  constructor(
    url: string,
    options: types.ICoreRequestOptions = {},
    requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null,
  ) {
    super(url, options);
    this.requestDataFunc = requestDataFunc;

    // Check for unsupported fetch-specific options
    if (
      options.credentials ||
      options.mode ||
      options.cache ||
      options.redirect ||
      options.referrer ||
      options.referrerPolicy ||
      options.integrity
    ) {
      throw new Error(
        'Fetch API specific options (credentials, mode, cache, redirect, referrer, referrerPolicy, integrity) are not supported in Node.js implementation',
      );
    }
  }

  /**
   * Fire the request and return a CoreResponse
   */
  async fire(): Promise<CoreResponse> {
    const incomingMessage = await this.fireCore();
    return new CoreResponse(incomingMessage, this.url, this.options);
  }

  /**
   * Fire the request and return the raw IncomingMessage
   */
  async fireCore(): Promise<plugins.http.IncomingMessage> {
    const done = plugins.smartpromise.defer<plugins.http.IncomingMessage>();

    // Parse URL
    const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(this.url, {
      searchParams: this.options.queryParams || {},
    });

    this.options.hostname = parsedUrl.hostname;
    if (parsedUrl.port) {
      this.options.port = parseInt(parsedUrl.port, 10);
    }
    this.options.path = parsedUrl.path;

    // Handle unix socket URLs
    if (CoreRequest.isUnixSocket(this.url)) {
      const { socketPath, path } = CoreRequest.parseUnixSocketUrl(this.url);
      this.options.socketPath = socketPath;
      this.options.path = path;
    }

    // Determine agent based on protocol and keep-alive setting
    if (!this.options.agent) {
      // Only use keep-alive agents if explicitly requested
      if (this.options.keepAlive === true) {
        this.options.agent =
          parsedUrl.protocol === 'https:' ? httpsAgent : httpAgent;
      } else if (this.options.keepAlive === false) {
        this.options.agent =
          parsedUrl.protocol === 'https:'
            ? httpsAgentKeepAliveFalse
            : httpAgentKeepAliveFalse;
      }
      // If keepAlive is undefined, don't set any agent (more fetch-like behavior)
    }

    // Determine request module
    const requestModule =
      parsedUrl.protocol === 'https:' ? plugins.https : plugins.http;

    if (!requestModule) {
      throw new Error(
        `The request to ${this.url} is missing a viable protocol. Must be http or https`,
      );
    }

    if (this.options.requestBody instanceof MultipartFormData) {
      this.options.requestBody.resolveMissingContentTypes(
        plugins.smartmime.getMimeTypeForPathSync,
      );
      this.options.headers = {
        ...this.options.headers,
        'content-length': String(this.options.requestBody.getContentLength()),
      };
    }

    // Perform the request
    let timeoutId: NodeJS.Timeout | null = null;
    const request = requestModule.request(this.options, async (response) => {
      // Handle hard timeout
      if (this.options.hardDataCuttingTimeout) {
        timeoutId = setTimeout(() => {
          response.destroy();
          done.reject(new Error('Request timed out'));
        }, this.options.hardDataCuttingTimeout);
      }

      // Always return the raw stream
      done.resolve(response);
    });

    // Set request timeout (Node.js built-in timeout)
    if (this.options.timeout) {
      request.setTimeout(this.options.timeout, () => {
        request.destroy();
        done.reject(new Error('Request timed out'));
      });
    }

    // Handle request errors before starting the body stream.
    request.on('error', (error) => {
      request.destroy();
      if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      done.reject(error);
    });

    // Write request body
    if (this.options.requestBody) {
      if (this.options.requestBody instanceof MultipartFormData) {
        const multipartStream = plugins.stream.Readable.from(
          this.options.requestBody.getChunks(),
          { objectMode: false },
        );
        plugins.stream.pipeline(multipartStream, request, (error) => {
          if (error) {
            done.reject(error);
          }
        });
      } else if (this.options.requestBody instanceof ReadableStream) {
        // Convert web ReadableStream to Node.js Readable stream
        const nodeStream = plugins.stream.Readable.fromWeb(this.options.requestBody as any);
        nodeStream.pipe(request);
      } else {
        // Write body as-is - caller is responsible for serialization
        const bodyData =
          typeof this.options.requestBody === 'string'
            ? this.options.requestBody
            : this.options.requestBody instanceof Buffer
              ? this.options.requestBody
              : JSON.stringify(this.options.requestBody); // Still stringify for backward compatibility
        request.end(bodyData);
      }
    } else if (this.requestDataFunc) {
      this.requestDataFunc(request);
    } else {
      request.end();
    }

    // Get response and handle response errors
    const response = await done.promise;
    
    // Clear timeout on successful response
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
    
    response.on('error', (err) => {
      console.error(err);
      response.destroy();
    });

    return response;
  }

  /**
   * Static factory method to create and fire a request
   */
  static async create(
    url: string,
    options: types.ICoreRequestOptions = {},
  ): Promise<CoreResponse> {
    const request = new CoreRequest(url, options);
    return request.fire();
  }
}
