import * as http from 'node:http';
import * as https from 'node:https';
import type { Plugin } from 'vite';

import {
  getRequestPathname,
  readErrorString,
  readRequestBody,
  serializeErrorForLog,
} from './utils/httpUtils';
import { AXURE_BRIDGE_BASE_URL } from './utils/makeConstants';
import {
  buildAxureBridgeUnavailablePayload,
  formatAxureProxyErrorDetails,
  limitErrorText,
  normalizeAxvgPayloadText,
} from './utils/proxyUtils';

type UpstreamResponse = {
  status: number;
  statusText: string;
  headers: http.IncomingHttpHeaders;
  bodyText: string;
};

const AVAILABILITY_PROBE_LOG_INTERVAL_MS = 30_000;

let lastAvailabilityProbeLogKey = '';
let lastAvailabilityProbeLogAt = 0;

function readHeaderValue(value: string | string[] | undefined): string {
  if (Array.isArray(value)) {
    return value.join(', ');
  }
  return String(value || '');
}

function logAvailabilityProbeFailure(payload: Record<string, unknown>) {
  const now = Date.now();
  const key = JSON.stringify(payload);
  if (key === lastAvailabilityProbeLogKey && now - lastAvailabilityProbeLogAt < AVAILABILITY_PROBE_LOG_INTERVAL_MS) {
    return;
  }

  lastAvailabilityProbeLogKey = key;
  lastAvailabilityProbeLogAt = now;
  console.warn('[axure-bridge-proxy] availability probe failed', payload);
}

async function requestAxureBridge(
  upstreamUrl: string,
  options: {
    method: 'GET' | 'POST';
    headers?: Record<string, string>;
    body?: Buffer;
    timeoutMs?: number;
  },
): Promise<UpstreamResponse> {
  const targetUrl = new URL(upstreamUrl);
  const transport = targetUrl.protocol === 'https:' ? https : http;

  return new Promise((resolve, reject) => {
    const request = transport.request(
      {
        protocol: targetUrl.protocol,
        hostname: targetUrl.hostname,
        port: targetUrl.port ? Number(targetUrl.port) : undefined,
        path: `${targetUrl.pathname}${targetUrl.search}`,
        method: options.method,
        headers: {
          Connection: 'close',
          ...options.headers,
        },
        agent: false,
      },
      (response) => {
        const chunks: Buffer[] = [];

        response.on('data', (chunk: Buffer | string) => {
          chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
        });
        response.on('end', () => {
          resolve({
            status: response.statusCode || 502,
            statusText: response.statusMessage || '',
            headers: response.headers,
            bodyText: Buffer.concat(chunks).toString('utf8'),
          });
        });
        response.on('error', reject);
      },
    );

    request.setTimeout(options.timeoutMs ?? 15_000, () => {
      request.destroy(new Error(`Axure Bridge request timed out after ${options.timeoutMs ?? 15_000}ms`));
    });
    request.on('error', reject);

    if (options.body && options.body.length > 0) {
      request.write(options.body);
    }

    request.end();
  });
}

async function requestAxureBridgeWithRetry(
  upstreamUrl: string,
  options: {
    method: 'GET' | 'POST';
    headers?: Record<string, string>;
    body?: Buffer;
    timeoutMs?: number;
  },
): Promise<UpstreamResponse> {
  try {
    return await requestAxureBridge(upstreamUrl, options);
  } catch (error: any) {
    const code = error?.code || error?.cause?.code;
    if (code !== 'ECONNRESET') {
      throw error;
    }

    return requestAxureBridge(upstreamUrl, options);
  }
}

export function axureBridgeProxyPlugin(): Plugin {
  return {
    name: 'axure-bridge-proxy-plugin',
    configureServer(server: any) {
      server.middlewares.use(async (req: any, res: any, next: any) => {
        const pathname = getRequestPathname(req);
        const isAvailableRoute = req.method === 'GET' && pathname === '/api/axure-bridge/available';
        const isCopyRoute = req.method === 'POST' && pathname === '/api/axure-bridge/copyaxvg';

        if (!isAvailableRoute && !isCopyRoute) {
          return next();
        }

        const upstreamUrl = isAvailableRoute
          ? `${AXURE_BRIDGE_BASE_URL}/available`
          : `${AXURE_BRIDGE_BASE_URL}/copyaxvg`;
        let payloadBytes = 0;

        try {
          let upstreamResponse: UpstreamResponse;

          if (isAvailableRoute) {
            upstreamResponse = await requestAxureBridgeWithRetry(upstreamUrl, {
              method: 'GET',
              timeoutMs: 5_000,
            });
          } else {
            let rawBody = '';
            try {
              rawBody = await readRequestBody(req);
            } catch (error: any) {
              res.statusCode = 400;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: error?.message || 'Invalid request body' }));
              return;
            }

            const requestBody = normalizeAxvgPayloadText(rawBody);
            const requestBuffer = Buffer.from(requestBody, 'utf8');
            payloadBytes = requestBuffer.byteLength;

            upstreamResponse = await requestAxureBridgeWithRetry(upstreamUrl, {
              method: 'POST',
              headers: {
                'Content-Type': 'text/plain; charset=utf-8',
                'Content-Length': String(payloadBytes),
              },
              body: requestBuffer,
              timeoutMs: 30_000,
            });
          }

          const contentType = readHeaderValue(upstreamResponse.headers['content-type']).toLowerCase();
          const responseText = upstreamResponse.bodyText;

          if (upstreamResponse.status < 200 || upstreamResponse.status >= 300) {
            if (isAvailableRoute) {
              const unavailablePayload = buildAxureBridgeUnavailablePayload({
                route: pathname,
                method: req.method,
                bridgeUrl: upstreamUrl,
                payloadBytes: payloadBytes || undefined,
                status: upstreamResponse.status,
                statusText: upstreamResponse.statusText,
                responseText: readErrorString(responseText) || upstreamResponse.statusText,
              });

              logAvailabilityProbeFailure({
                route: pathname,
                method: req.method,
                upstreamUrl,
                status: upstreamResponse.status,
                statusText: upstreamResponse.statusText,
                bodyPreview: limitErrorText(readErrorString(responseText), 300) || undefined,
              });

              res.statusCode = 200;
              res.setHeader('Cache-Control', 'no-store');
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify(unavailablePayload));
              return;
            }

            console.warn('[axure-bridge-proxy] upstream responded with error', {
              route: pathname,
              method: req.method,
              upstreamUrl,
              payloadBytes: payloadBytes || undefined,
              status: upstreamResponse.status,
              statusText: upstreamResponse.statusText,
              bodyPreview: limitErrorText(readErrorString(responseText), 800) || undefined,
            });
          }

          res.statusCode = upstreamResponse.status;
          res.setHeader('Cache-Control', 'no-store');

          if (contentType.includes('application/json')) {
            res.setHeader('Content-Type', 'application/json; charset=utf-8');
            res.end(responseText || '{}');
            return;
          }

          if (responseText) {
            try {
              const parsed = JSON.parse(responseText);
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify(parsed));
              return;
            } catch {
              // Pass through non-JSON text responses.
            }
          }

          res.setHeader('Content-Type', 'text/plain; charset=utf-8');
          res.end(responseText);
        } catch (error: any) {
          const errorLog = serializeErrorForLog(error);
          if (isAvailableRoute) {
            const unavailablePayload = buildAxureBridgeUnavailablePayload({
              route: pathname,
              method: req.method,
              bridgeUrl: upstreamUrl,
              payloadBytes: payloadBytes || undefined,
              error,
            });

            logAvailabilityProbeFailure({
              route: pathname,
              method: req.method,
              upstreamUrl,
              payloadBytes: payloadBytes || undefined,
              error: {
                message: errorLog.message,
                code: errorLog.code || errorLog.causeCode || undefined,
                causeMessage: errorLog.causeMessage,
              },
            });

            res.statusCode = 200;
            res.setHeader('Cache-Control', 'no-store');
            res.setHeader('Content-Type', 'application/json; charset=utf-8');
            res.end(JSON.stringify(unavailablePayload));
            return;
          }

          console.error('[axure-bridge-proxy] upstream request failed', {
            route: pathname,
            method: req.method,
            upstreamUrl,
            payloadBytes: payloadBytes || undefined,
            error: errorLog,
          });

          res.statusCode = 502;
          res.setHeader('Content-Type', 'application/json; charset=utf-8');
          res.end(JSON.stringify({
            error: error?.message || 'Axure Bridge unavailable',
            details: formatAxureProxyErrorDetails(error),
            code: errorLog.code || errorLog.causeCode || undefined,
            causeMessage: errorLog.causeMessage || undefined,
            route: pathname,
            method: req.method,
            bridgeUrl: upstreamUrl,
            payloadBytes: payloadBytes || undefined,
          }));
        }
      });
    },
  };
}
