import { FastifyReply, FastifyRequest } from 'fastify';
import { request as undiciRequest } from 'undici';
import WS, { WebSocketServer, type RawData } from 'ws';

import { config } from '../config';
import { processResponseHeaders } from '../proxy/headers';
import {
  appendUserMessage,
  extractUserTextFromBody,
  looksLikeUserMessageRequest,
} from '../support/service';
import { parseNewMessageSubscribe, trackClientSubscription, injectCustomerMessage, injectWelcomeMessage } from '../support/zowieWsInject';
import { enrichGraphqlResponseBody } from '../support/zowieHistoryEnrich';
import {
  buildBlockedMutationResponse,
  filterGraphqlResponseBody,
  parseBlockedUserMutation,
  shouldForwardUpstreamWsFrame,
} from '../support/zowieFilter';

const ZOWIE_HOST = 'eu1.chat.getzowie.com';

function rewriteZowieBody(text: string): string {
  const origin = config.origin;
  const prefix = config.intercept.zowieProxyPrefix;
  return text
    .replaceAll(`https://${ZOWIE_HOST}`, `${origin}${prefix}`)
    .replaceAll(`http://${ZOWIE_HOST}`, `${origin}${prefix}`)
    .replaceAll(`//${ZOWIE_HOST}`, `${prefix}`);
}

function parseCookieHeader(raw: string | undefined): Record<string, string> {
  const out: Record<string, string> = {};
  if (!raw) return out;
  for (const part of raw.split(';')) {
    const idx = part.indexOf('=');
    if (idx <= 0) continue;
    const key = part.slice(0, idx).trim();
    const val = part.slice(idx + 1).trim();
    if (key) out[key] = decodeURIComponent(val);
  }
  return out;
}

function parseJsonBody(raw: Buffer | string | null | undefined): unknown {
  if (!raw) return null;
  const text = Buffer.isBuffer(raw) ? raw.toString('utf8') : raw;
  if (!text.trim()) return null;
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

async function interceptCoreRestPost(
  request: FastifyRequest,
  body: Buffer | null,
): Promise<{ intercept: boolean; responseBody: string } | null> {
  if (!config.support.enabled) return null;

  const parsed = parseJsonBody(body);
  if (!parsed || !looksLikeUserMessageRequest(parsed)) {
    return null;
  }

  const text = extractUserTextFromBody(parsed);
  if (!text) return null;

  const visitorUuid =
    (request.cookies as Record<string, string>)[config.session.visitorCookieName] ?? undefined;
  const referer = (request.headers.referer as string | undefined) ?? '';

  try {
    await appendUserMessage({
      visitorUuid,
      pageUrl: referer,
      text,
    });
  } catch (err) {
    console.error('[zowie-proxy] appendUserMessage:', (err as Error).message);
  }

  return {
    intercept: true,
    responseBody: JSON.stringify({ ok: true, intercepted: true }),
  };
}

async function interceptGraphqlUserPost(
  request: FastifyRequest,
  body: Buffer | null,
): Promise<{ intercept: boolean; responseBody: string; headers?: Record<string, string> } | null> {
  if (!config.support.enabled || !body) return null;

  const parsed = parseJsonBody(body);
  const userMsg = parseBlockedUserMutation(parsed);
  if (!userMsg) return null;

  const visitorUuid =
    (request.cookies as Record<string, string>)[config.session.visitorCookieName] ?? undefined;
  const referer = (request.headers.referer as string | undefined) ?? '';

  let supportSessionId: string | undefined;
  try {
    const saved = await appendUserMessage({
      visitorUuid,
      pageUrl: referer,
      text: userMsg.text,
      zowieSessionKey: userMsg.conversationId,
      conversationId: userMsg.conversationId,
    });
    if (saved.sessionId) supportSessionId = saved.sessionId;
  } catch (err) {
    console.error('[zowie-proxy] graphql user message:', (err as Error).message);
  }

  if (userMsg.conversationId) {
    const pushed = injectCustomerMessage(userMsg.conversationId, userMsg.text, visitorUuid);
    if (pushed > 0) {
      console.log(`[zowie-proxy] user message pushed via WS to ${pushed} client(s)`);
    } else {
      console.warn(
        `[zowie-proxy] user message saved but no active WS subscription (conversation=${userMsg.conversationId.slice(0, 8)})`,
      );
    }
  }

  return {
    intercept: true,
    responseBody: buildBlockedMutationResponse(userMsg.field, userMsg.text),
    headers: supportSessionId ? { 'X-Support-Session-Id': supportSessionId } : undefined,
  };
}

function buildUpstreamHeaders(request: FastifyRequest): Record<string, string> {
  const headers: Record<string, string> = {
    accept: (request.headers.accept as string) ?? '*/*',
    'accept-encoding': 'identity',
    'user-agent':
      (request.headers['user-agent'] as string) ??
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    origin: config.origin,
    referer: (request.headers.referer as string) ?? `${config.origin}/pl/pl/`,
  };

  for (const key of ['content-type', 'authorization', 'cookie', 'accept-language'] as const) {
    const val = request.headers[key];
    if (typeof val === 'string' && val) {
      headers[key] = val;
    }
  }

  for (const [key, val] of Object.entries(request.headers)) {
    if (!key.startsWith('x-') || typeof val !== 'string' || !val) continue;
    headers[key] = val;
  }

  return headers;
}

export async function handleZowieProxy(
  request: FastifyRequest<{ Params: { '*': string } }>,
  reply: FastifyReply,
  upstreamPathOverride?: string,
): Promise<void> {
  const suffix = upstreamPathOverride ?? request.params['*'] ?? '';
  const upstreamUrl = `${config.support.zowieUpstream}/${suffix.replace(/^\//, '')}`;
  const method = (request.method ?? 'GET').toUpperCase();
  const isCoreRest = suffix.startsWith('api/v1/core/rest');
  const isGraphql = suffix.startsWith('api/v1/core/graphql');

  let body: Buffer | null = null;
  if (request.body != null) {
    if (Buffer.isBuffer(request.body)) {
      body = request.body.length ? request.body : null;
    } else if (typeof request.body === 'string') {
      body = request.body.length ? Buffer.from(request.body, 'utf8') : null;
    } else {
      body = Buffer.from(JSON.stringify(request.body), 'utf8');
    }
  }

  if (method === 'POST' && isCoreRest && !suffix.includes('/chat/events')) {
    const intercepted = await interceptCoreRestPost(request, body);
    if (intercepted?.intercept) {
      reply
        .header('content-type', 'application/json; charset=utf-8')
        .header('access-control-allow-origin', '*')
        .status(200)
        .send(intercepted.responseBody);
      return;
    }
  }

  if (method === 'POST' && isGraphql && body) {
    const intercepted = await interceptGraphqlUserPost(request, body);
    if (intercepted?.intercept) {
      reply
        .header('content-type', 'application/json; charset=utf-8')
        .header('access-control-allow-origin', request.headers.origin ?? '*')
        .header('access-control-allow-credentials', 'true')
        .header('access-control-expose-headers', 'X-Support-Session-Id');
      if (intercepted.headers) {
        for (const [key, value] of Object.entries(intercepted.headers)) {
          reply.header(key, value);
        }
      }
      reply.status(200).send(intercepted.responseBody);
      return;
    }
  }

  const headers = buildUpstreamHeaders(request);

  const { statusCode, headers: upHeaders, body: upBody } = await undiciRequest(upstreamUrl, {
    method: method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS',
    headers,
    body: method === 'GET' || method === 'HEAD' ? undefined : body,
    throwOnError: false,
  });

  const chunks: Buffer[] = [];
  for await (const chunk of upBody) {
    chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
  }
  let responseBody = Buffer.concat(chunks);

  const contentType = String(upHeaders['content-type'] ?? '');
  const rewritable =
    contentType.includes('javascript') ||
    contentType.includes('json') ||
    contentType.includes('html') ||
    contentType.includes('css');

  if (rewritable) {
    responseBody = Buffer.from(rewriteZowieBody(responseBody.toString('utf8')), 'utf8');
  }

  if (isGraphql && contentType.includes('json')) {
    try {
      responseBody = Buffer.from(
        await enrichGraphqlResponseBody(responseBody, body ? parseJsonBody(body) : null),
      );
    } catch (err) {
      console.error('[zowie-proxy] enrichGraphqlResponseBody failed:', (err as Error).message);
      responseBody = Buffer.from(
        filterGraphqlResponseBody(responseBody).toString('utf8'),
        'utf8',
      );
    }
  }

  const outHeaders = processResponseHeaders(upHeaders as Record<string, string | string[]>);
  outHeaders['access-control-allow-origin'] = request.headers.origin ?? '*';
  outHeaders['access-control-allow-credentials'] = 'true';
  if (contentType.includes('javascript') || contentType.includes('css')) {
    outHeaders['cache-control'] = 'public, max-age=300, stale-while-revalidate=300';
  }

  for (const [k, v] of Object.entries(outHeaders)) {
    reply.header(k, v as string);
  }
  reply.status(statusCode).send(responseBody);
}

/** Proxy Zowie paths mounted at site root (when widget JS uses same origin). */
export async function handleZowieRootProxy(
  request: FastifyRequest,
  reply: FastifyReply,
): Promise<void> {
  const url = new URL(request.url, config.origin);
  const path = url.pathname.replace(/^\//, '') + url.search;
  await handleZowieProxy(
    request as FastifyRequest<{ Params: { '*': string } }>,
    reply,
    path,
  );
}

/** Attach WebSocket proxy for Zowie GraphQL subscriptions. */
export function attachZowieWebSocketProxy(server: import('http').Server): void {
  if (!config.support.enabled) return;

  const wss = new WebSocketServer({ noServer: true });
  const upstreamBase = config.support.zowieUpstream.replace(/^http/i, 'ws');

  server.on('upgrade', (req, socket, head) => {
    const url = req.url ?? '';
    if (!url.startsWith('/api/v1/core/graphql-ws')) {
      return;
    }

    wss.handleUpgrade(req, socket, head, (clientWs) => {
      const cookies = parseCookieHeader(req.headers.cookie);
      const visitorUuid = cookies[config.session.visitorCookieName];
      const upstreamUrl = `${upstreamBase}/api/v1/core/graphql-ws${url.includes('?') ? url.slice(url.indexOf('?')) : ''}`;
      const upstreamWs = new WS(upstreamUrl, {
        headers: {
          origin: config.origin,
          referer: req.headers.referer ?? `${config.origin}/web/chat-core/`,
          'user-agent':
            req.headers['user-agent'] ??
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
          ...(req.headers.authorization ? { authorization: req.headers.authorization } : {}),
          ...(req.headers.cookie ? { cookie: req.headers.cookie } : {}),
        },
      });

      const forwardToUpstream = (data: RawData, isBinary: boolean) => {
        const send = () => upstreamWs.send(data, { binary: isBinary });
        if (upstreamWs.readyState === WS.OPEN) send();
        else upstreamWs.once('open', send);
      };

      clientWs.on('message', (data, isBinary) => {
        forwardToUpstream(data, isBinary);
        if (!isBinary) {
          const raw =
            typeof data === 'string'
              ? data
              : Buffer.isBuffer(data)
                ? data
                : Buffer.from(data as ArrayBuffer);
          const sub = parseNewMessageSubscribe(raw);
          if (sub) {
            trackClientSubscription(clientWs, sub.subscriptionId, sub.conversationId, visitorUuid);
            // Push welcome with composer enabled — upstream AI frames are blocked.
            injectWelcomeMessage(sub.conversationId);
          }
        }
      });
      upstreamWs.on('message', (data, isBinary) => {
        if (clientWs.readyState !== WS.OPEN) return;
        if (!isBinary) {
          const raw =
            typeof data === 'string'
              ? data
              : Buffer.isBuffer(data)
                ? data
                : Buffer.from(data as ArrayBuffer);
          if (!shouldForwardUpstreamWsFrame(raw)) return;
        }
        clientWs.send(data, { binary: isBinary });
      });

      const closeBoth = () => {
        try {
          clientWs.close();
        } catch {
          /* ignore */
        }
        try {
          upstreamWs.close();
        } catch {
          /* ignore */
        }
      };

      clientWs.on('close', closeBoth);
      upstreamWs.on('close', closeBoth);
      clientWs.on('error', closeBoth);
      upstreamWs.on('error', closeBoth);
    });
  });
}

export function rewriteZowieUrls(text: string): string {
  if (!config.support.enabled) return text;
  const origin = config.origin;
  const prefix = config.intercept.zowieProxyPrefix;
  return text
    .replaceAll(`https://${ZOWIE_HOST}`, `${origin}${prefix}`)
    .replaceAll(`http://${ZOWIE_HOST}`, `${origin}${prefix}`)
    .replaceAll(`//${ZOWIE_HOST}`, `${prefix}`);
}
