'use strict';

const http = require('http');

function getPort() {
  return Number(process.env.WALL_E_PORT || '3457');
}

function getToken() {
  return process.env.WALL_E_API_TOKEN || '';
}

function requestJson(pathname, {
  method = 'GET',
  body = null,
  port = getPort(),
  headers = {},
  token = getToken(),
  signal = null,
} = {}) {
  return new Promise((resolve, reject) => {
    const payload = body == null ? null : JSON.stringify(body);
    const req = http.request({
      host: '127.0.0.1',
      port,
      path: pathname,
      method,
      headers: {
        ...(payload ? {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(payload),
        } : {}),
        ...(token ? { authorization: `Bearer ${token}` } : {}),
        ...headers,
      },
    }, (res) => {
      const chunks = [];
      res.on('data', (chunk) => chunks.push(chunk));
      res.on('end', () => {
        const raw = Buffer.concat(chunks).toString();
        let json = null;
        try { json = raw ? JSON.parse(raw) : null; } catch {}
        resolve({ status: res.statusCode || 0, headers: res.headers, body: raw, json });
      });
    });
    req.on('error', reject);
    if (signal) {
      if (signal.aborted) req.destroy(new Error('Cancelled'));
      else signal.addEventListener('abort', () => req.destroy(new Error('Cancelled')), { once: true });
    }
    if (payload) req.write(payload);
    req.end();
  });
}

function requestSse(pathname, {
  method = 'GET',
  body = null,
  port = getPort(),
  headers = {},
  token = getToken(),
  signal = null,
  onEvent = () => {},
} = {}) {
  return new Promise((resolve, reject) => {
    const payload = body == null ? null : JSON.stringify(body);
    const events = [];
    let raw = '';
    let buffer = '';
    let settled = false;
    let resRef = null;
    const fail = (err) => {
      if (settled) return;
      settled = true;
      try { if (resRef && !resRef.destroyed) resRef.destroy(); } catch {}
      try { if (!req.destroyed) req.destroy(); } catch {}
      if (signal?.aborted || err.message === 'Cancelled') reject(new Error('Cancelled'));
      else reject(err);
    };
    const succeed = (response) => {
      if (settled) return;
      settled = true;
      resolve(response);
    };
    const req = http.request({
      host: '127.0.0.1',
      port,
      path: pathname,
      method,
      headers: {
        Accept: 'text/event-stream',
        ...(payload ? {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(payload),
        } : {}),
        ...(token ? { authorization: `Bearer ${token}` } : {}),
        ...headers,
      },
    }, (res) => {
      resRef = res;
      res.on('error', fail);
      const processFrame = (frame) => {
        if (settled) return false;
        const dataLines = frame.split(/\r?\n/)
          .filter((line) => line.startsWith('data:'))
          .map((line) => line.slice(5).trimStart());
        if (!dataLines.length) return true;
        const data = dataLines.join('\n');
        try {
          const event = JSON.parse(data);
          events.push(event);
          try { onEvent(event); } catch (err) { fail(err); return false; }
        } catch (err) {
          events.push({ type: 'parse_error', raw: data, error: err.message });
        }
        return true;
      };

      res.on('data', (chunk) => {
        if (settled) return;
        const text = chunk.toString();
        raw += text;
        buffer += text;
        let idx;
        while ((idx = buffer.indexOf('\n\n')) >= 0) {
          const frame = buffer.slice(0, idx);
          buffer = buffer.slice(idx + 2);
          if (!processFrame(frame)) break;
        }
      });
      res.on('end', () => {
        if (settled) return;
        if (buffer.trim() && !processFrame(buffer)) return;
        succeed({ status: res.statusCode || 0, headers: res.headers, body: raw, events });
      });
    });
    req.on('error', fail);
    if (signal) {
      if (signal.aborted) req.destroy(new Error('Cancelled'));
      else signal.addEventListener('abort', () => req.destroy(new Error('Cancelled')), { once: true });
    }
    if (payload) req.write(payload);
    req.end();
  });
}

// Strip request headers that belong to the CTM<->browser session and must not
// leak into Wall-E's request context. The CTM `Authorization` token would
// otherwise show up verbatim in any Wall-E middleware that inspects headers
// (logs, audit, future per-request quotas, etc.). Cookies follow the same rule.
function _sanitizeHeaders(headers) {
  const out = { ...headers };
  delete out.authorization;
  delete out.Authorization;
  delete out.cookie;
  delete out.Cookie;
  return out;
}

function _truncate(value, max = 500) {
  const text = String(value || '');
  return text.length > max ? `${text.slice(0, max)}...` : text;
}

function _buildUnavailablePayload(err, context = null) {
  const status = context && typeof context === 'object' ? context : {};
  const nativeDependency = status.nativeDependency || status.native_dependency || null;
  const lastStartError = status.lastStartError || status.last_start_error || null;
  const statusError = status.error || lastStartError?.message || (nativeDependency && nativeDependency.ok === false ? nativeDependency.message : null);
  const reason = statusError || err.message || String(err);
  const code = status.code
    || lastStartError?.code
    || (nativeDependency && nativeDependency.ok === false ? nativeDependency.code : null)
    || 'walle_unavailable';
  return {
    ok: false,
    code,
    error: `Wall-E unavailable: ${_truncate(reason)}`,
    walle: {
      running: !!status.running,
      pid: status.pid || null,
      pid_source: status.pidSource || status.pid_source || null,
      stale_pid: status.stalePid || status.stale_pid || null,
      code,
      error: _truncate(reason),
      native_dependency: nativeDependency || null,
      last_start_error: lastStartError || null,
    },
  };
}

function proxyHttp(req, res, { pathname, port = getPort(), token = getToken(), unavailableContext = null } = {}) {
  return new Promise((resolve) => {
    const upstreamHeaders = _sanitizeHeaders(req.headers);
    upstreamHeaders.host = `127.0.0.1:${port}`;
    if (token) upstreamHeaders.authorization = `Bearer ${token}`;
    let responseStarted = false;
    let settled = false;
    const finish = () => {
      if (settled) return;
      settled = true;
      resolve(true);
    };

    const upstream = http.request({
      host: '127.0.0.1',
      port,
      path: pathname || req.url,
      method: req.method,
      headers: upstreamHeaders,
    }, (upstreamRes) => {
      responseStarted = true;
      res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers);
      upstreamRes.pipe(res);
      upstreamRes.on('end', finish);
    });
    upstream.on('error', (err) => {
      if (responseStarted || res.headersSent || res.writableEnded || res.destroyed) {
        finish();
        return;
      }
      Promise.resolve()
        .then(() => (typeof unavailableContext === 'function' ? unavailableContext(err) : unavailableContext))
        .catch(() => null)
        .then((context) => {
          if (res.headersSent || res.writableEnded || res.destroyed) {
            finish();
            return;
          }
          const body = JSON.stringify(_buildUnavailablePayload(err, context));
          res.writeHead(502, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
          res.end(body);
          finish();
        });
    });
    req.pipe(upstream);
  });
}

module.exports = {
  getPort,
  getToken,
  proxyHttp,
  requestJson,
  requestSse,
  _sanitizeHeaders,
  _buildUnavailablePayload,
};
