import ky from 'ky';
import { config } from '../config.js';
import { buildApiKeyAuthHeader, getAuthHeader } from './auth.ts';
import { logApiError } from './error-logger.ts';
import { logPerfEvent } from './perf-logger.ts';
import { interceptDemoRequest, isDemoMode } from './demo-mode';
import { createCircuitBreaker, parseRetryAfterMs } from './circuit-breaker.js';

// Action-specific timeouts for better performance
const TIMEOUT_BY_ACTION = {
  Folders: 5000,
  FolderGet: 5000,
  FolderCreate: 5000,
  FolderUpdate: 5000,
  FolderDelete: 5000,
  Labels: 5000,
  MessageList: 10000,
  Contacts: 10000,
  ContactsCreate: 10000,
  Message: 20000,
  MessageUpdate: 15000,
  MessageDelete: 10000,
  CalendarEvents: 15000,
  Emails: 30000,
  Account: 10000,
  AccountUpdate: 15000,
  default: 30000,
};

const addJitter = (delay) => delay + Math.random() * 1000;

// ── Global 401 interception ──────────────────────────────────────────
// Track consecutive auth failures across different actions.  When the
// threshold is reached, dispatch a global event so the app can force
// re-authentication instead of silently failing on every request.
const AUTH_FAILURE_THRESHOLD = 3;
const AUTH_FAILURE_COOLDOWN_MS = 30_000; // Don't fire more than once per 30s
let _consecutiveAuthFailures = 0;
let _lastAuthFailureDispatch = 0;

function recordAuthSuccess() {
  _consecutiveAuthFailures = 0;
}

function recordAuthFailure() {
  _consecutiveAuthFailures++;
  if (
    _consecutiveAuthFailures >= AUTH_FAILURE_THRESHOLD &&
    Date.now() - _lastAuthFailureDispatch > AUTH_FAILURE_COOLDOWN_MS
  ) {
    _lastAuthFailureDispatch = Date.now();
    _consecutiveAuthFailures = 0;
    // Dispatch on next microtick so the throwing caller can finish first
    Promise.resolve().then(() => {
      window.dispatchEvent(new CustomEvent('fe:auth-expired'));
    });
  }
}

// ── Global request circuit breaker ───────────────────────────────────
// Under a degraded environment (e.g. a struggling WKWebView whose db worker
// fell back to the main thread) reads time out, the retry layer multiplies
// each into more attempts, and the backend gets hammered. After a burst of
// transient failures (timeouts / 5xx / 429) the breaker opens and we fail GETs
// fast for a short cooldown so the client stops piling on. Mutations
// (POST/PUT/DELETE) bypass it — they're low-volume and user-initiated — as
// does any caller that passes { bypassCircuit: true }. The sync worker runs an
// independent instance of this same breaker over its own raw-fetch path.
const requestCircuit = createCircuitBreaker();

// Create base ky instance with default configuration
const api = ky.create({
  prefixUrl: config.apiBase,
  timeout: 30000, // 30 second timeout (default, overridden per-action)
  retry: {
    // Keep retries modest: a degraded WKWebView times every request out and
    // each retry multiplies backend load. Two retries max, idempotent methods
    // only (never re-send a POST on timeout — risk of a duplicate create), and
    // 429 is intentionally NOT here — retrying a rate-limit fights the backend;
    // the circuit breaker honors it (via Retry-After) instead.
    limit: 2,
    methods: ['get', 'head', 'put', 'delete'],
    statusCodes: [0, 408, 500, 502, 503, 504],
    backoffLimit: 5000,
    delay: (attemptCount) => addJitter(Math.min(1000 * Math.pow(2, attemptCount - 1), 5000)),
  },
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
});

export const Remote = {
  async request(action, params = {}, options = {}) {
    // Demo mode: intercept all requests with fake data
    if (isDemoMode()) {
      const demo = interceptDemoRequest(action, params, options);
      if (demo.handled) {
        // Write actions return { blocked: true } — throw so callers
        // enter their error path.  The demo toast was already shown.
        if (demo.result?.blocked) {
          const err = new Error('This action is not available in the demo');
          err.name = 'DemoBlockedError';
          err.code = 'DEMO_ACTION_BLOCKED';
          err.isDemo = true;
          throw err;
        }
        return demo.result;
      }
    }

    const { path, method: defaultMethod } = this.getEndpoint(action);
    const method = (options.method || defaultMethod || 'GET').toLowerCase();
    const { signal } = options;

    // Circuit breaker: when tripped, fail GETs fast instead of adding to the
    // storm. Mutations and explicit { bypassCircuit } callers go through.
    if (method === 'get' && !options.bypassCircuit && requestCircuit.isOpen()) {
      const err = new Error('Backing off — service temporarily unavailable');
      err.status = 503;
      err.circuitOpen = true;
      throw err;
    }
    const perfLabel = options.perfLabel || action;
    const perfStart = performance?.now ? performance.now() : null;

    // Build URL path
    const urlPath = (options.pathOverride || path).replace(/^\//, ''); // Remove leading slash for ky

    // Prepare headers
    const headers = { ...options.headers };

    // Handle authorization
    if (!options.skipAuth && this.shouldAuthorize(action)) {
      if (options.apiKey) {
        headers.Authorization = buildApiKeyAuthHeader(options.apiKey);
      } else {
        headers.Authorization = getAuthHeader({ allowApiKey: false, required: true });
      }
    }

    // Prepare ky options
    const kyOptions = {
      method,
      headers,
      signal,
      timeout: options.timeout ?? TIMEOUT_BY_ACTION[action] ?? TIMEOUT_BY_ACTION.default,
      hooks: {
        afterResponse: [
          async (request, options, response) => {
            // Log API errors for feedback system
            if (!response.ok) {
              const contentType = response.headers.get('content-type') || '';
              const isJson = contentType.includes('application/json');
              let errorData = null;

              try {
                errorData = isJson ? await response.clone().json() : null;
              } catch (e) {
                // Failed to parse JSON
                logApiError(action, method.toUpperCase(), 0, e);
              }

              // Sanitize error message to prevent server info leakage
              const rawMessage = errorData?.message || errorData?.error || 'Request failed';
              // Strip server-specific details (stack traces, file paths, internal IPs)
              const message =
                typeof rawMessage === 'string'
                  ? rawMessage
                      .replace(
                        /\b(?:\/[\w./-]+|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|at\s+\S+)\b/g,
                        '[redacted]',
                      )
                      .slice(0, 500)
                  : 'Request failed';
              const error = new Error(message);
              // Only copy safe, known fields from error data
              if (errorData && typeof errorData === 'object') {
                const safeFields = ['code', 'description', 'param', 'type'];
                for (const field of safeFields) {
                  if (field in errorData) error[field] = errorData[field];
                }
              }
              error.status = response.status;

              logApiError(action, method.toUpperCase(), response.status, error);
            }
            return response;
          },
        ],
      },
    };

    // Handle params based on method
    if (method === 'get') {
      // Filter out empty values for GET requests
      const searchParams = {};
      Object.entries(params || {}).forEach(([key, value]) => {
        if (value !== undefined && value !== null && value !== '') {
          searchParams[key] = value;
        }
      });
      kyOptions.searchParams = searchParams;
    } else {
      // For POST/PUT/DELETE, send as JSON body
      kyOptions.json = params || {};
    }

    try {
      const response = await api(urlPath, kyOptions);

      // Successful response — reset consecutive auth failure counter and
      // close the circuit breaker.
      recordAuthSuccess();
      requestCircuit.recordSuccess();

      // Allow callers to explicitly request text (e.g. raw RFC822 EML downloads)
      if (options.responseAs === 'text') {
        return await response.text();
      }

      // ky automatically parses JSON if content-type is application/json
      // For non-JSON responses, we need to handle differently
      const contentType = response.headers.get('content-type') || '';
      const isJson = contentType.includes('application/json');

      if (isJson) {
        return await response.json();
      }

      // Fallback: return raw text for any non-JSON response rather than null
      // so callers that expected a body get something useful (e.g. message/rfc822)
      return await response.text();
    } catch (error) {
      console.error(`Remote.${action} failed:`, error);

      // ky throws HTTPError for non-2xx responses
      // For network errors, error.name will be 'TypeError'
      if (error.name === 'HTTPError') {
        // HTTP error - already logged in afterResponse hook
        const err = new Error(error.message);
        err.status = error.response.status;
        err.isAuthError = error.response.status === 401 || error.response.status === 403;

        // Track consecutive auth failures for global interception
        if (err.isAuthError) {
          recordAuthFailure();
        }

        // Feed the circuit breaker: 429 (honor Retry-After) and 5xx mean the
        // backend is rate-limiting or struggling — back off rather than pile on.
        // Any other status (4xx incl. 404) proves the backend is alive — close it.
        if (err.status === 429) {
          requestCircuit.recordFailure(parseRetryAfterMs(error.response?.headers));
        } else if (err.status >= 500) {
          requestCircuit.recordFailure();
        } else {
          requestCircuit.recordSuccess();
        }

        // Try to get error details from response
        try {
          const errorData = await error.response.json();
          if (errorData && typeof errorData === 'object') {
            Object.assign(err, errorData);
            err.message = errorData.message || errorData.error || error.message;
          }
        } catch (e) {
          // Couldn't parse JSON, use original error
          logApiError(action, method.toUpperCase(), 0, e);
        }

        throw err;
      } else if (error.name === 'TimeoutError') {
        // Timeout error — counts toward the circuit breaker (a degraded env
        // times everything out; backing off is what protects the backend).
        requestCircuit.recordFailure();
        const err = new Error('Request timeout');
        err.status = 408;
        logApiError(action, method.toUpperCase(), 408, err);
        throw err;
      } else {
        // Network error or other error (no status code)
        requestCircuit.recordFailure();
        logApiError(action, method.toUpperCase(), 0, error);
        throw error;
      }
    } finally {
      if (perfStart && typeof performance !== 'undefined' && performance.now) {
        const duration = performance.now() - perfStart;
        logPerfEvent('api.request', {
          action,
          method: method.toUpperCase(),
          path: options.pathOverride || path,
          duration,
          label: perfLabel,
        });
      }
    }
  },

  getEndpoint(action) {
    const endpoints = {
      Folders: { path: '/v1/folders', method: 'GET' },
      FolderCreate: { path: '/v1/folders', method: 'POST' },
      FolderGet: { path: '/v1/folders/:id', method: 'GET' },
      FolderUpdate: { path: '/v1/folders/:id', method: 'PUT' },
      FolderDelete: { path: '/v1/folders/:id', method: 'DELETE' },
      MessageList: { path: '/v1/messages', method: 'GET' },
      Message: { path: '/v1/messages', method: 'GET' },
      MessageUpdate: { path: '/v1/messages', method: 'PUT' },
      MessageDelete: { path: '/v1/messages', method: 'DELETE' },
      Contacts: { path: '/v1/contacts', method: 'GET' },
      ContactsCreate: { path: '/v1/contacts', method: 'POST' },
      ContactsUpdate: { path: '/v1/contacts', method: 'PUT' },
      ContactsDelete: { path: '/v1/contacts', method: 'DELETE' },
      Calendars: { path: '/v1/calendars', method: 'GET' },
      Calendar: { path: '/v1/calendars/:id', method: 'GET' },
      CalendarCreate: { path: '/v1/calendars', method: 'POST' },
      CalendarUpdate: { path: '/v1/calendars/:id', method: 'PUT' },
      CalendarDelete: { path: '/v1/calendars/:id', method: 'DELETE' },
      CalendarEvents: { path: '/v1/calendar-events', method: 'GET' },
      CalendarEventCreate: { path: '/v1/calendar-events', method: 'POST' },
      CalendarEventUpdate: { path: '/v1/calendar-events', method: 'PUT' },
      CalendarEventDelete: { path: '/v1/calendar-events', method: 'DELETE' },
      Labels: { path: '/v1/labels', method: 'GET' },
      LabelsCreate: { path: '/v1/labels', method: 'POST' },
      LabelsUpdate: { path: '/v1/labels', method: 'PUT' },
      Emails: { path: '/v1/emails', method: 'POST' },
      EmailCancel: { path: '/v1/emails/:id', method: 'DELETE' },
      Account: { path: '/v1/account', method: 'GET' },
      AccountUpdate: { path: '/v1/account', method: 'PUT' },
    };

    const entry = endpoints[action];
    if (entry) return entry;
    return { path: `/v1/${String(action || '').toLowerCase()}`, method: 'GET' };
  },

  shouldAuthorize(action) {
    if (!action) return true;
    return action !== 'Login';
  },
};
