import { telemetry } from './app-telemetry.js';

const JSON_ACCEPT = 'application/json';
const DEFAULT_HEADERS = Object.freeze({
  'Accept': JSON_ACCEPT,
  'X-Requested-With': 'XMLHttpRequest',
});

const toObject = (value) => {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return {};
  }
  return value;
};

const normalizeUrl = (value) => {
  try {
    return new URL(String(value || ''), window.location.href).toString();
  } catch {
    return String(value || '');
  }
};

const normalizeRoute = (url) => {
  try {
    return new URL(url, window.location.href).pathname || '/';
  } catch {
    return '/';
  }
};

const mergeHeaders = (baseHeaders, inputHeaders) => {
  const headers = new Headers();
  Object.entries(baseHeaders).forEach(([key, value]) => {
    headers.set(key, value);
  });

  if (!inputHeaders) {
    return headers;
  }

  if (inputHeaders instanceof Headers) {
    inputHeaders.forEach((value, key) => headers.set(key, value));
    return headers;
  }

  if (Array.isArray(inputHeaders)) {
    inputHeaders.forEach((entry) => {
      if (Array.isArray(entry) && entry.length >= 2) {
        headers.set(String(entry[0]), String(entry[1]));
      }
    });
    return headers;
  }

  if (typeof inputHeaders === 'object') {
    Object.entries(inputHeaders).forEach(([key, value]) => {
      if (value === null || value === undefined) {
        return;
      }
      headers.set(String(key), String(value));
    });
  }

  return headers;
};

const readJsonSafe = async (response) => {
  const raw = await response.text();
  if (raw.trim() === '') {
    return null;
  }

  return JSON.parse(raw);
};

const messageFromPayload = (payload, fallback) => {
  if (!payload || typeof payload !== 'object') {
    return fallback;
  }

  if (typeof payload.message === 'string' && payload.message.trim() !== '') {
    return payload.message.trim();
  }

  if (typeof payload.error === 'string' && payload.error.trim() !== '') {
    return payload.error.trim();
  }

  if (payload.errors && typeof payload.errors === 'object') {
    const general = payload.errors.general;
    if (typeof general === 'string' && general.trim() !== '') {
      return general.trim();
    }
  }

  return fallback;
};

const normalizeMethod = (value, fallback = 'GET') => {
  const method = String(value || fallback).trim().toUpperCase();
  return method === '' ? fallback : method;
};

const buildHttpErrorMessage = (method, status, message) => {
  if (status > 0) {
    return `[${method}] HTTP ${status}: ${message}`;
  }
  return `[${method}] ${message}`;
};

export class HttpError extends Error {
  constructor(details = {}) {
    const method = normalizeMethod(details.method || 'GET');
    const status = Number.isFinite(Number(details.status)) ? Number(details.status) : 0;
    const message = String(details.message || 'Request failed').trim() || 'Request failed';
    super(buildHttpErrorMessage(method, status, message));
    this.name = 'HttpError';
    this.status = status;
    this.method = method;
    this.url = normalizeUrl(details.url || '');
    this.payload = details.payload ?? null;
    this.cause = details.cause ?? null;
  }
}

export class SessionExpiredError extends Error {
  constructor(redirectUrl = '') {
    super('Session expired');
    this.name = 'SessionExpiredError';
    this.redirectUrl = redirectUrl;
  }
}

const emitHttpTelemetry = (error, source) => {
  if (!(error instanceof HttpError)) {
    return;
  }

  telemetry.capture('ajax_error', {
    severity: error.status >= 500 || error.status === 0 ? 'error' : 'warning',
    message: error.message,
    meta: {
      source,
      module: 'app-http',
      http_method: error.method,
      http_status: error.status,
      request_path: normalizeRoute(error.url),
      error_code: error.status === 0 ? 'http_error_status_0' : `http_${error.status}`,
    },
  });
};

const toFormBody = (data) => {
  if (data instanceof URLSearchParams) {
    return data;
  }

  if (typeof FormData !== 'undefined' && data instanceof FormData) {
    return new URLSearchParams(data);
  }

  if (data && typeof data === 'object') {
    const body = new URLSearchParams();
    Object.entries(data).forEach(([key, value]) => {
      if (value === null || value === undefined) {
        body.append(key, '');
      } else {
        body.append(key, String(value));
      }
    });
    return body;
  }

  return new URLSearchParams();
};

const performRequest = async (url, options = {}) => {
  const normalizedUrl = normalizeUrl(url);
  const method = normalizeMethod(options.method || 'GET');
  const headers = mergeHeaders(DEFAULT_HEADERS, options.headers || null);

  const requestInit = {
    ...options,
    method,
    credentials: options.credentials || 'same-origin',
    headers,
  };

  let response;
  try {
    response = await fetch(normalizedUrl, requestInit);
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      throw error;
    }

    const httpError = new HttpError({
      status: 0,
      method,
      url: normalizedUrl,
      message: 'Network error',
      payload: null,
      cause: error,
    });
    emitHttpTelemetry(httpError, 'http.network');
    throw httpError;
  }

  let payload = null;
  try {
    payload = await readJsonSafe(response);
  } catch (error) {
    const httpError = new HttpError({
      status: 0,
      method,
      url: normalizedUrl,
      message: 'Invalid JSON response',
      payload: null,
      cause: error,
    });
    emitHttpTelemetry(httpError, 'http.parse');
    throw httpError;
  }

  if (!response.ok) {
    const message = messageFromPayload(payload, response.statusText || 'Request failed');
    const httpError = new HttpError({
      status: response.status,
      method,
      url: normalizedUrl,
      message,
      payload,
    });
    emitHttpTelemetry(httpError, 'http.response');
    throw httpError;
  }

  return payload;
};

export const getJson = (url, options = {}) => performRequest(url, {
  ...options,
  method: 'GET',
});

export const postForm = (url, data, options = {}) => {
  const body = toFormBody(data);
  const headers = mergeHeaders({
    ...DEFAULT_HEADERS,
    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
  }, options.headers || null);

  return performRequest(url, {
    ...options,
    method: 'POST',
    headers,
    body,
  });
};

export const getHtml = async (url, options = {}) => {
  const normalizedUrl = normalizeUrl(url);
  const headers = mergeHeaders({
    'Accept': 'text/html',
    'X-Requested-With': 'XMLHttpRequest',
  }, options.headers || null);

  let response;
  try {
    response = await fetch(normalizedUrl, {
      ...options,
      method: 'GET',
      credentials: options.credentials || 'same-origin',
      headers,
    });
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') { throw error; }
    const httpError = new HttpError({
      status: 0,
      method: 'GET',
      url: normalizedUrl,
      message: 'Network error',
      cause: error,
    });
    emitHttpTelemetry(httpError, 'http.network');
    throw httpError;
  }

  if (!response.ok) {
    const httpError = new HttpError({
      status: response.status,
      method: 'GET',
      url: normalizedUrl,
      message: response.statusText || 'Request failed',
    });
    emitHttpTelemetry(httpError, 'http.response');
    throw httpError;
  }

  // Session-expiry detection: fetch follows 302 redirects transparently, so we
  // land on the login page with a 200 response. The final URL path diverges
  // from the requested one — that's the signal.
  try {
    const requested = new URL(normalizedUrl).pathname;
    const landed = new URL(response.url || normalizedUrl).pathname;
    if (landed !== requested && /\/login(\/|$)/.test(landed)) {
      throw new SessionExpiredError(response.url || '');
    }
  } catch (err) {
    if (err instanceof SessionExpiredError) { throw err; }
    // URL parsing error — ignore, let caller process the body
  }

  return response.text();
};

export const postJson = (url, payload, options = {}) => {
  const headers = mergeHeaders({
    ...DEFAULT_HEADERS,
    'Content-Type': 'application/json;charset=UTF-8',
  }, options.headers || null);

  return performRequest(url, {
    ...options,
    method: 'POST',
    headers,
    body: JSON.stringify(payload ?? {}),
  });
};
