'use strict';

/**
 * status-box.js — Mattermost post manager.
 *
 * Features:
 *   - Shared http.Agent (keepAlive, maxSockets)
 *   - createPost / updatePost with circuit breaker
 *   - Throttle: leading-edge fires immediately, trailing flush after THROTTLE_MS
 *   - Message size guard (truncate to MAX_MESSAGE_CHARS)
 *   - Retry with exponential backoff on 429/5xx (up to MAX_RETRIES)
 *   - Structured logs for every API call
 */

const https = require('https');
const http = require('http');
const { EventEmitter } = require('events');
const { CircuitBreaker } = require('./circuit-breaker');

const DEFAULT_THROTTLE_MS = 500;
const DEFAULT_MAX_CHARS = 15000;
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_MAX_SOCKETS = 4;

class StatusBox extends EventEmitter {
  /**
   * @param {object} opts
   * @param {string} opts.baseUrl - Mattermost base URL
   * @param {string} opts.token - Bot token
   * @param {object} [opts.logger] - pino logger
   * @param {number} [opts.throttleMs]
   * @param {number} [opts.maxMessageChars]
   * @param {number} [opts.maxRetries]
   * @param {number} [opts.maxSockets]
   * @param {CircuitBreaker} [opts.circuitBreaker]
   */
  constructor(opts) {
    super();
    this.baseUrl = opts.baseUrl;
    this.token = opts.token;
    this.logger = opts.logger || null;
    this.throttleMs = opts.throttleMs || DEFAULT_THROTTLE_MS;
    this.maxMessageChars = opts.maxMessageChars || DEFAULT_MAX_CHARS;
    this.maxRetries = opts.maxRetries || DEFAULT_MAX_RETRIES;

    const parsedUrl = new URL(this.baseUrl);
    this.hostname = parsedUrl.hostname;
    this.port = parsedUrl.port
      ? parseInt(parsedUrl.port, 10)
      : parsedUrl.protocol === 'https:'
        ? 443
        : 80;
    this.isHttps = parsedUrl.protocol === 'https:';

    const maxSockets = opts.maxSockets || DEFAULT_MAX_SOCKETS;
    this.agent = new (this.isHttps ? https : http).Agent({
      keepAlive: true,
      maxSockets,
    });

    this.circuitBreaker =
      opts.circuitBreaker ||
      new CircuitBreaker({
        threshold: 5,
        cooldownMs: 30000,
        logger: this.logger,
      });

    // Metrics
    this.metrics = {
      updatesSent: 0,
      updatesFailed: 0,
      queueDepth: 0,
    };

    // Throttle state per postId
    // Map<postId, { pending: string|null, timer: NodeJS.Timeout|null, lastFiredAt: number }>
    this._throttleState = new Map();
  }

  /**
   * Create a new Mattermost post.
   * @param {string} channelId
   * @param {string} text
   * @param {string} [rootId] - Thread root post ID
   * @returns {Promise<string>} Post ID
   */
  async createPost(channelId, text, rootId) {
    const body = { channel_id: channelId, message: this._truncate(text) };
    if (rootId) body.root_id = rootId;

    const post = await this._apiCall('POST', '/api/v4/posts', body);
    if (this.logger) this.logger.debug({ postId: post.id, channelId }, 'Created status post');
    this.metrics.updatesSent++;

    return post.id;
  }

  /**
   * Update a Mattermost post (throttled).
   * Leading edge fires immediately; subsequent calls within throttleMs are batched.
   * Guaranteed trailing flush when activity stops.
   *
   * @param {string} postId
   * @param {string} text
   * @returns {Promise<void>}
   */
  updatePost(postId, text) {
    return new Promise((resolve, reject) => {
      let state = this._throttleState.get(postId);
      if (!state) {
        state = { pending: null, timer: null, lastFiredAt: 0, resolvers: [] };
        this._throttleState.set(postId, state);
      }

      state.resolvers.push({ resolve, reject });
      state.pending = text;
      this.metrics.queueDepth = this._throttleState.size;

      const now = Date.now();
      const elapsed = now - state.lastFiredAt;

      if (elapsed >= this.throttleMs && !state.timer) {
        // Leading edge: fire immediately
        this._flushUpdate(postId);
      } else {
        // Trailing flush: schedule if not already scheduled
        if (!state.timer) {
          state.timer = setTimeout(() => {
            this._flushUpdate(postId);
          }, this.throttleMs - elapsed);
        }
        // If timer already scheduled, pending text was updated above — it will flush latest text
      }
    });
  }

  /**
   * Flush the pending update for a postId.
   * @private
   * Note: PUT updates cause Mattermost to show '(edited)' label on the post.
   * This is a known API limitation. The Mattermost plugin (Phase 3) solves this
   * via custom post type rendering.
   */
  async _flushUpdate(postId) {
    const state = this._throttleState.get(postId);
    if (!state || state.pending === null) return;

    const text = state.pending;
    const resolvers = [...state.resolvers];
    state.pending = null;
    state.resolvers = [];
    state.lastFiredAt = Date.now();
    if (state.timer) {
      clearTimeout(state.timer);
      state.timer = null;
    }

    this.metrics.queueDepth = Math.max(0, this.metrics.queueDepth - 1);

    try {
      // In-place PUT update
      await this._apiCallWithRetry('PUT', `/api/v4/posts/${postId}`, {
        id: postId,
        message: this._truncate(text),
      });
      this.metrics.updatesSent++;
      resolvers.forEach(({ resolve }) => resolve());
    } catch (err) {
      this.metrics.updatesFailed++;
      resolvers.forEach(({ reject }) => reject(err));
    }
  }

  /**
   * Force-flush any pending update for a postId (used on shutdown).
   * @param {string} postId
   * @returns {Promise<void>}
   */
  async forceFlush(postId) {
    const state = this._throttleState.get(postId);
    if (!state) return;
    if (state.timer) {
      clearTimeout(state.timer);
      state.timer = null;
    }
    if (state.pending !== null) {
      await this._flushUpdate(postId);
    }
  }

  /**
   * Force-flush all pending updates.
   * @returns {Promise<void>}
   */
  async flushAll() {
    const postIds = [...this._throttleState.keys()];
    await Promise.allSettled(postIds.map((id) => this.forceFlush(id)));
  }

  /**
   * Truncate text to maxMessageChars.
   * @private
   */
  _truncate(text) {
    if (text.length <= this.maxMessageChars) return text;
    const suffix = '\n...(truncated)';
    return text.slice(0, this.maxMessageChars - suffix.length) + suffix;
  }

  /**
   * Make an API call through the circuit breaker with retries.
   * @private
   */
  async _apiCallWithRetry(method, path, body) {
    return this.circuitBreaker.execute(() => this._retryApiCall(method, path, body));
  }

  /**
   * Make an API call directly (no circuit breaker, for createPost).
   * @private
   */
  async _apiCall(method, apiPath, body) {
    return this.circuitBreaker.execute(() => this._retryApiCall(method, apiPath, body));
  }

  /**
   * Retry logic for API calls.
   * @private
   */
  async _retryApiCall(method, apiPath, body, attempt = 0) {
    try {
      return await this._httpRequest(method, apiPath, body);
    } catch (err) {
      const isRetryable = err.statusCode === 429 || (err.statusCode >= 500 && err.statusCode < 600);
      if (isRetryable && attempt < this.maxRetries) {
        const delayMs = Math.min(1000 * Math.pow(2, attempt), 10000);
        if (this.logger) {
          this.logger.warn(
            { attempt, delayMs, statusCode: err.statusCode },
            'API call failed, retrying',
          );
        }
        await sleep(delayMs);
        return this._retryApiCall(method, apiPath, body, attempt + 1);
      }
      throw err;
    }
  }

  /**
   * Low-level HTTP request.
   * @private
   */
  _httpRequest(method, apiPath, body) {
    const transport = this.isHttps ? https : http;
    const bodyStr = body ? JSON.stringify(body) : null;

    return new Promise((resolve, reject) => {
      const reqOpts = {
        hostname: this.hostname,
        port: this.port,
        path: apiPath,
        method,
        agent: this.agent,
        headers: {
          Authorization: `Bearer ${this.token}`,
          'Content-Type': 'application/json',
        },
      };
      if (bodyStr) {
        reqOpts.headers['Content-Length'] = Buffer.byteLength(bodyStr);
      }

      const req = transport.request(reqOpts, (res) => {
        let data = '';
        res.on('data', (chunk) => (data += chunk));
        res.on('end', () => {
          if (res.statusCode >= 200 && res.statusCode < 300) {
            try {
              resolve(data ? JSON.parse(data) : {});
            } catch (_e) {
              resolve({});
            }
          } else {
            let msg = `HTTP ${res.statusCode}`;
            try {
              msg = JSON.parse(data).message || msg;
            } catch (_e) {
              /* use default */
            }
            const err = new Error(msg);
            err.statusCode = res.statusCode;
            reject(err);
          }
        });
      });

      req.on('error', reject);
      if (bodyStr) req.write(bodyStr);
      req.end();
    });
  }

  getMetrics() {
    return {
      ...this.metrics,
      circuit: this.circuitBreaker.getMetrics(),
    };
  }

  destroy() {
    this.agent.destroy();
    // Clear all throttle timers
    for (const [, state] of this._throttleState) {
      if (state.timer) clearTimeout(state.timer);
    }
    this._throttleState.clear();
  }
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

module.exports = { StatusBox };
