/**
 * DashCaddy JavaScript Client — lightweight SDK for the DashCaddy API.
 *
 * Zero external dependencies. Works in Node.js 18+ (uses global fetch).
 * Full TypeScript definitions in types.d.ts.
 *
 * @example
 * const { DashCaddyClient } = require('./dashcaddy-client');
 *
 * // API key auth (simplest — no CSRF needed)
 * const client = new DashCaddyClient({
 *   baseUrl: 'https://status.sami',
 *   apiKey: 'dk_abc123_xyz'
 * });
 *
 * // Session cookie auth (CSRF handled automatically)
 * const client2 = new DashCaddyClient({
 *   baseUrl: 'https://status.sami',
 *   sessionCookie: 'sid=...'
 * });
 *
 * const services = await client.services.list();       // GET /api/v1/services
 * const health = await client.health.get();             // GET /health
 * const { containers } = await client.containers.discover();
 * await client.dns.createRecord({ type: 'A', domain: 'app.sami', value: '10.0.0.1' });
 * const { backup } = await client.backups.execute();
 *
 * @license MIT
 */

'use strict';

const DEFAULT_TIMEOUT = 30000;
const DEFAULT_MAX_RETRIES = 3;
const RETRY_BASE_MS = 500;
const API_PREFIX = '/api/v1';
const CSRF_HEADER = 'x-csrf-token';
const API_KEY_HEADER = 'x-api-key';

/** Error thrown on non-success API responses or network failures after retries. */
class DashCaddyError extends Error {
  constructor(message, statusCode, code, details) {
    super(message);
    this.name = 'DashCaddyError';
    this.statusCode = statusCode || 0;
    this.code = code;
    this.details = details;
  }
}

// ── Client ─────────────────────────────────────────────────────

class DashCaddyClient {
  /**
   * @param {object} options
   * @param {string} options.baseUrl - Base URL, e.g. 'https://status.sami'.
   * @param {string} [options.apiKey] - API key (dk_<id>_<secret>). Bypasses CSRF.
   * @param {string} [options.sessionCookie] - Session cookie value for cookie auth.
   * @param {string} [options.csrfToken] - Pre-fetched CSRF token.
   * @param {number} [options.timeout=30000] - Request timeout in ms.
   * @param {number} [options.maxRetries=3] - Max retries on 5xx.
   * @param {Record<string,string>} [options.headers] - Extra default headers.
   * @param {typeof fetch} [options.fetch] - Custom fetch implementation.
   */
  constructor(options) {
    if (!options || !options.baseUrl) throw new Error('DashCaddyClient: baseUrl is required');
    this.baseUrl = options.baseUrl.replace(/\/+$/, '');
    this.apiKey = options.apiKey || null;
    this.sessionCookie = options.sessionCookie || null;
    this._csrfToken = options.csrfToken || null;
    this.timeout = options.timeout || DEFAULT_TIMEOUT;
    this.maxRetries = options.maxRetries !== undefined ? options.maxRetries : DEFAULT_MAX_RETRIES;
    this.extraHeaders = options.headers || {};
    this._fetchImpl = options.fetch || null;
    this._useApiKey = !!this.apiKey;

    // Resource namespaces — defined via compact spec tables below
    this.services = this._buildResource(SERVICES_SPEC);
    this.containers = this._buildResource(CONTAINERS_SPEC);
    this.health = this._buildResource(HEALTH_SPEC);
    this.dns = this._buildResource(DNS_SPEC);
    this.backups = this._buildResource(BACKUPS_SPEC);
    this.config = this._buildResource(CONFIG_SPEC);
    this.monitoring = this._buildResource(MONITORING_SPEC);
  }

  /**
   * Build a resource namespace from a compact method spec.
   * Each spec entry: [methodName, httpMethod, pathTemplate, needsBody, isRoot]
   * pathTemplate uses :param placeholders substituted from args[0..n].
   * isRoot=true means the path is root-level (no /api/v1 prefix), e.g. /health.
   * @private
   */
  _buildResource(spec) {
    const client = this;
    const obj = {};
    for (const entry of spec) {
      const [name, httpMethod, pathTpl, hasBody, isRoot] = entry;
      obj[name] = async function (...args) {
        let path = pathTpl;
        // Substitute :param placeholders from positional args (strings/numbers only)
        const params = pathTpl.match(/:[\w]+/g) || [];
        let argIdx = 0;
        for (const param of params) {
          if (argIdx < args.length) {
            path = path.replace(param, encodeURIComponent(String(args[argIdx++])));
          }
        }
        // Body or query is the next arg after path params
        const nextArg = args[argIdx];
        const opts = { root: isRoot || false };
        if (hasBody) opts.body = nextArg || {};
        else if (nextArg && typeof nextArg === 'object') opts.query = nextArg;
        return client._request(httpMethod, path, opts);
      };
    }
    return obj;
  }

  /**
   * Fetch and cache a CSRF token (session-cookie auth only).
   * @returns {Promise<string|null>}
   */
  async ensureCsrfToken() {
    if (this._useApiKey) return null;
    if (this._csrfToken) return this._csrfToken;
    try {
      const res = await this._request('GET', '/csrf-token', { _skipCsrf: true });
      this._csrfToken = res.token || null;
      return this._csrfToken;
    } catch (_) { return null; }
  }

  /**
   * Core request: builds URL + headers, handles auth, retries 5xx.
   * @param {string} method - HTTP method.
   * @param {string} path - Path after API prefix (or root-level if opts.root).
   * @param {object} [opts] - { body, query, root, _skipCsrf, signal }.
   * @returns {Promise<object>} Parsed response (success envelope spread).
   * @private
   */
  async _request(method, path, opts = {}) {
    const { body, query, root, _skipCsrf, signal } = opts;

    // Build URL
    let url = `${this.baseUrl}${root ? '' : API_PREFIX}${path}`;
    if (query) {
      const qs = new URLSearchParams(
        Object.entries(query).filter(([, v]) => v !== undefined && v !== null)
      ).toString();
      if (qs) url += `?${qs}`;
    }

    // CSRF: needed for state-changing requests in session-cookie mode
    const isStateChanging = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase());
    const needsCsrf = isStateChanging && !_skipCsrf && !this._useApiKey;
    let csrfToken = this._csrfToken;
    if (needsCsrf && !csrfToken) csrfToken = await this.ensureCsrfToken();

    // Headers
    const headers = { 'Content-Type': 'application/json', ...this.extraHeaders };
    if (this._useApiKey) headers[API_KEY_HEADER] = this.apiKey;
    if (this.sessionCookie) headers['Cookie'] = this.sessionCookie;
    if (csrfToken && !_skipCsrf) headers[CSRF_HEADER] = csrfToken;

    // Retry loop
    let lastError;
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        const res = await this._fetch(url, method, headers, body, signal);
        const text = await res.text();
        let json = null;
        if (text) { try { json = JSON.parse(text); } catch (_) { json = { success: res.ok, raw: text }; } }

        // Retry on 5xx
        if (res.status >= 500 && attempt < this.maxRetries) {
          await this._backoff(attempt);
          continue;
        }

        // Envelope check
        if (json && json.success === false) {
          throw new DashCaddyError(json.error || `Status ${res.status}`, res.status, json.code, json);
        }
        if (!res.ok && !(json && json.success === true)) {
          throw new DashCaddyError((json && json.error) || `HTTP ${res.status}`, res.status, json && json.code, json);
        }
        return json || { success: true };

      } catch (err) {
        if (err instanceof DashCaddyError) {
          if (err.statusCode >= 500 && attempt < this.maxRetries) { lastError = err; await this._backoff(attempt); continue; }
          throw err;
        }
        lastError = err;
        if (attempt < this.maxRetries) { await this._backoff(attempt); continue; }
        throw new DashCaddyError(
          err.name === 'AbortError' ? `Timeout after ${this.timeout}ms` : `Network error: ${err.message}`,
          0, 'NETWORK_ERROR', { originalError: err.message }
        );
      }
    }
    throw lastError || new DashCaddyError('Request failed after all retries', 0);
  }

  /** Low-level fetch with timeout. @private */
  async _fetch(url, method, headers, body, externalSignal) {
    const fetchFn = this._fetchImpl || fetch;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeout);
    if (externalSignal) {
      if (externalSignal.aborted) controller.abort();
      else externalSignal.addEventListener('abort', () => controller.abort(), { once: true });
    }
    try {
      return await fetchFn(url, {
        method, headers,
        body: body !== undefined ? JSON.stringify(body) : undefined,
        signal: controller.signal,
      });
    } finally { clearTimeout(timer); }
  }

  /** Exponential backoff with jitter. @private */
  async _backoff(attempt) {
    const delay = RETRY_BASE_MS * Math.pow(2, attempt - 1);
    await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.3));
  }

  // ── Auth & System Helpers ──

  /** Exchange API key for JWT. POST /api/v1/auth/jwt */
  async exchangeJwt(apiKey) {
    const key = apiKey || this.apiKey;
    if (!key) throw new DashCaddyError('API key required', 0, 'NO_API_KEY');
    return this._request('POST', '/auth/jwt', { body: { apiKey: key }, _skipCsrf: true });
  }

  /** Verify TOTP and establish session. POST /api/v1/totp/verify */
  async verifyTotp(code) {
    const res = await this._request('POST', '/totp/verify', { body: { code }, _skipCsrf: true });
    if (res.csrfToken) this._csrfToken = res.csrfToken;
    return res;
  }

  /** Get API version. GET /api/v1/version */
  async version() { return this._request('GET', '/version'); }

  /** Get metrics summary. GET /api/v1/metrics */
  async metrics() { return this._request('GET', '/metrics'); }
}

// ── Resource Specs ─────────────────────────────────────────────
// [methodName, httpMethod, pathTemplate, hasBody]
// Path params (:id) are filled from positional string/number args.
// For hasBody=true, the arg after path params is the body.
// For hasBody=false, an object arg after path params is treated as query params.

const SERVICES_SPEC = [
  ['list',          'GET',    '/services',           false],
  ['status',        'GET',    '/services/status',    false],
  ['create',        'POST',   '/services',           true],
  ['updateAll',     'PUT',    '/services',           true],
  ['delete',        'DELETE', '/services/:id',       false],
  ['triggerUpdate', 'POST',   '/services/update',    true],
];

const CONTAINERS_SPEC = [
  ['discover',    'GET',    '/containers/discover',          false],
  ['logs',        'GET',    '/containers/:id/logs',          false],
  ['resources',   'GET',    '/containers/:id/resources',     false],
  ['checkUpdate', 'GET',    '/containers/:id/check-update',  false],
  ['start',       'POST',   '/containers/:id/start',         true],
  ['stop',        'POST',   '/containers/:id/stop',          true],
  ['restart',     'POST',   '/containers/:id/restart',       true],
  ['update',      'POST',   '/containers/:id/update',        true],
  ['remove',      'DELETE', '/containers/:id',               false],
];

const HEALTH_SPEC = [
  ['get',      'GET', '/health',           false, true],
  ['live',     'GET', '/health/live',      false, true],
  ['ready',    'GET', '/health/ready',     false, true],
  ['services', 'GET', '/health/services',  false],
  ['cached',   'GET', '/health/cached',    false],
  ['service',  'GET', '/health/service/:id', false],
  ['ca',       'GET', '/health/ca',        false],
];

const DNS_SPEC = [
  ['providers',          'GET',    '/dns/providers',                 false],
  ['providerStatus',     'GET',    '/dns/provider/status',           false],
  ['createRecord',       'POST',   '/dns/record',                    true],
  ['createUniversal',    'POST',   '/dns/universal/record',          true],
  ['deleteRecord',       'DELETE', '/dns/record',                    true],
  ['resolve',            'GET',    '/dns/resolve',                   false],
  ['credentials',        'GET',    '/dns/credentials',               false],
  ['setCredentials',     'POST',   '/dns/credentials',               true],
  ['propagation',        'GET',    '/dns/propagation/:domain',       false],
];

const BACKUPS_SPEC = [
  ['getConfig',    'GET',    '/backups/config',             false],
  ['updateConfig', 'POST',   '/backups/config',             true],
  ['execute',      'POST',   '/backups/execute',            true],
  ['history',      'GET',    '/backups/history',            false],
  ['storageInfo',  'GET',    '/backups/storage-info',       false],
  ['restore',      'POST',   '/backups/restore/:backupId',  true],
  ['files',        'GET',    '/backups/files',              false],
];

const CONFIG_SPEC = [
  ['get',    'GET',  '/config', false],
  ['update', 'POST', '/config', true],
];

const MONITORING_SPEC = [
  ['stats',            'GET',  '/monitoring/stats',                   false],
  ['containerStats',   'GET',  '/monitoring/stats/:containerId',      false],
  ['history',          'GET',  '/monitoring/history/:containerId',    false],
  ['alertConfig',      'GET',  '/monitoring/alerts/config',           false],
  ['updateAlertConfig','POST', '/monitoring/alerts/config',           true],
  ['alerts',           'GET',  '/monitoring/alerts',                  false],
];

module.exports = { DashCaddyClient, DashCaddyError };
