const wsService = require('./websocketService');

const MAX_RECENT_ACTIVITIES = 120;
const MAX_ACTIVITY_OUTPUT_CHARS = 12000;
const MAX_ACTIVITY_TEXT_CHARS = 2000;
const OUTPUT_BROADCAST_THROTTLE_MS = 180;

function nowIso() {
  return new Date().toISOString();
}

function normalizeNumber(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return null;
  }
  return Math.trunc(parsed);
}

function normalizeText(value, { trim = true, maxChars = MAX_ACTIVITY_TEXT_CHARS } = {}) {
  if (value === null || value === undefined) {
    return null;
  }
  let text = String(value);
  if (trim) {
    text = text.trim();
  }
  if (!text) {
    return null;
  }
  if (text.length > maxChars) {
    if (trim) {
      const suffix = ' ...[gekürzt]';
      text = `${text.slice(0, Math.max(0, maxChars - suffix.length))}${suffix}`;
    } else {
      const prefix = '...[gekürzt]\n';
      text = `${prefix}${text.slice(-Math.max(0, maxChars - prefix.length))}`;
    }
  }
  return text;
}

function normalizeOutputChunk(value) {
  if (value === null || value === undefined) {
    return '';
  }
  const normalized = String(value).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  if (!normalized) {
    return '';
  }
  return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
}

function appendOutputTail(currentValue, chunk, maxChars = MAX_ACTIVITY_OUTPUT_CHARS) {
  const normalizedChunk = normalizeOutputChunk(chunk);
  const currentText = currentValue == null ? '' : String(currentValue);
  if (!normalizedChunk) {
    return {
      value: currentText || null,
      truncated: false
    };
  }

  const combined = `${currentText}${normalizedChunk}`;
  if (combined.length <= maxChars) {
    return {
      value: combined,
      truncated: false
    };
  }

  return {
    value: combined.slice(-maxChars),
    truncated: true
  };
}

function sanitizeActivity(input = {}) {
  const source = input && typeof input === 'object' ? input : {};
  const normalizedOutcome = normalizeText(source.outcome, { trim: true, maxChars: 40 });
  return {
    id: normalizeNumber(source.id),
    type: String(source.type || '').trim().toLowerCase() || 'task',
    name: String(source.name || '').trim() || null,
    status: String(source.status || '').trim().toLowerCase() || 'running',
    source: String(source.source || '').trim() || null,
    message: String(source.message || '').trim() || null,
    currentStep: String(source.currentStep || '').trim() || null,
    currentStepType: String(source.currentStepType || '').trim() || null,
    currentScriptName: String(source.currentScriptName || '').trim() || null,
    stepIndex: normalizeNumber(source.stepIndex),
    stepTotal: normalizeNumber(source.stepTotal),
    parentActivityId: normalizeNumber(source.parentActivityId),
    jobId: normalizeNumber(source.jobId),
    cronJobId: normalizeNumber(source.cronJobId),
    chainId: normalizeNumber(source.chainId),
    scriptId: normalizeNumber(source.scriptId),
    canCancel: Boolean(source.canCancel),
    canNextStep: Boolean(source.canNextStep),
    outcome: normalizedOutcome ? String(normalizedOutcome).toLowerCase() : null,
    errorMessage: normalizeText(source.errorMessage, { trim: true, maxChars: MAX_ACTIVITY_TEXT_CHARS }),
    output: normalizeText(source.output, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
    stdout: normalizeText(source.stdout, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
    stderr: normalizeText(source.stderr, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
    outputTruncated: Boolean(source.outputTruncated),
    stdoutTruncated: Boolean(source.stdoutTruncated),
    stderrTruncated: Boolean(source.stderrTruncated),
    startedAt: source.startedAt || nowIso(),
    finishedAt: source.finishedAt || null,
    durationMs: Number.isFinite(Number(source.durationMs)) ? Number(source.durationMs) : null,
    exitCode: Number.isFinite(Number(source.exitCode)) ? Number(source.exitCode) : null,
    success: source.success === null || source.success === undefined ? null : Boolean(source.success)
  };
}

class RuntimeActivityService {
  constructor() {
    this.nextId = 1;
    this.active = new Map();
    this.recent = [];
    this.controls = new Map();
    this.outputBroadcastTimer = null;
  }

  buildSnapshot() {
    const active = Array.from(this.active.values())
      .sort((a, b) => String(b.startedAt || '').localeCompare(String(a.startedAt || '')));
    const recent = [...this.recent]
      .sort((a, b) => String(b.finishedAt || b.startedAt || '').localeCompare(String(a.finishedAt || a.startedAt || '')));
    return {
      active,
      recent,
      updatedAt: nowIso()
    };
  }

  broadcastSnapshot() {
    if (this.outputBroadcastTimer) {
      clearTimeout(this.outputBroadcastTimer);
      this.outputBroadcastTimer = null;
    }
    wsService.broadcast('RUNTIME_ACTIVITY_CHANGED', this.buildSnapshot());
  }

  scheduleOutputBroadcast() {
    if (this.outputBroadcastTimer) {
      return;
    }
    this.outputBroadcastTimer = setTimeout(() => {
      this.outputBroadcastTimer = null;
      wsService.broadcast('RUNTIME_ACTIVITY_CHANGED', this.buildSnapshot());
    }, OUTPUT_BROADCAST_THROTTLE_MS);
  }

  startActivity(type, payload = {}) {
    const id = this.nextId;
    this.nextId += 1;
    const activity = sanitizeActivity({
      ...payload,
      id,
      type,
      status: 'running',
      outcome: 'running',
      startedAt: payload?.startedAt || nowIso(),
      finishedAt: null,
      durationMs: null,
      canCancel: Boolean(payload?.canCancel),
      canNextStep: Boolean(payload?.canNextStep)
    });
    this.active.set(id, activity);
    this.broadcastSnapshot();
    return id;
  }

  updateActivity(activityId, patch = {}) {
    const id = normalizeNumber(activityId);
    if (!id || !this.active.has(id)) {
      return null;
    }
    const current = this.active.get(id);
    const next = sanitizeActivity({
      ...current,
      ...patch,
      id: current.id,
      type: current.type,
      status: current.status === 'running' ? (patch?.status || current.status) : current.status,
      startedAt: current.startedAt
    });
    this.active.set(id, next);
    this.broadcastSnapshot();
    return next;
  }

  appendActivityOutput(activityId, patch = {}) {
    const id = normalizeNumber(activityId);
    if (!id || !this.active.has(id)) {
      return null;
    }

    const current = this.active.get(id);
    const nextOutput = appendOutputTail(current.output, patch?.output, MAX_ACTIVITY_OUTPUT_CHARS);
    const nextStdout = appendOutputTail(current.stdout, patch?.stdout, MAX_ACTIVITY_OUTPUT_CHARS);
    const nextStderr = appendOutputTail(current.stderr, patch?.stderr, MAX_ACTIVITY_OUTPUT_CHARS);
    const next = sanitizeActivity({
      ...current,
      ...patch,
      id: current.id,
      type: current.type,
      status: current.status,
      startedAt: current.startedAt,
      output: nextOutput.value,
      stdout: nextStdout.value,
      stderr: nextStderr.value,
      outputTruncated: Boolean(current.outputTruncated || patch?.outputTruncated || nextOutput.truncated),
      stdoutTruncated: Boolean(current.stdoutTruncated || patch?.stdoutTruncated || nextStdout.truncated),
      stderrTruncated: Boolean(current.stderrTruncated || patch?.stderrTruncated || nextStderr.truncated)
    });
    this.active.set(id, next);
    this.scheduleOutputBroadcast();
    return next;
  }

  completeActivity(activityId, payload = {}) {
    const id = normalizeNumber(activityId);
    if (!id || !this.active.has(id)) {
      return null;
    }
    const current = this.active.get(id);
    const finishedAt = payload?.finishedAt || nowIso();
    const startedAtDate = new Date(current.startedAt);
    const finishedAtDate = new Date(finishedAt);
    const durationMs = Number.isFinite(startedAtDate.getTime()) && Number.isFinite(finishedAtDate.getTime())
      ? Math.max(0, finishedAtDate.getTime() - startedAtDate.getTime())
      : null;
    const status = String(payload?.status || '').trim().toLowerCase() || (payload?.success === false ? 'error' : 'success');
    let outcome = String(payload?.outcome || '').trim().toLowerCase();
    if (!outcome) {
      if (Boolean(payload?.cancelled)) {
        outcome = 'cancelled';
      } else if (Boolean(payload?.skipped)) {
        outcome = 'skipped';
      } else {
        outcome = status === 'success' ? 'success' : 'error';
      }
    }
    const finalized = sanitizeActivity({
      ...current,
      ...payload,
      id: current.id,
      type: current.type,
      status,
      outcome,
      canCancel: false,
      canNextStep: false,
      finishedAt,
      durationMs
    });
    this.active.delete(id);
    this.controls.delete(id);
    this.recent.unshift(finalized);
    if (this.recent.length > MAX_RECENT_ACTIVITIES) {
      this.recent = this.recent.slice(0, MAX_RECENT_ACTIVITIES);
    }
    this.broadcastSnapshot();
    return finalized;
  }

  getSnapshot() {
    return this.buildSnapshot();
  }

  clearRecent() {
    const removed = this.recent.length;
    if (removed === 0) {
      return { removed: 0, snapshot: this.buildSnapshot() };
    }
    this.recent = [];
    this.broadcastSnapshot();
    return {
      removed,
      snapshot: this.buildSnapshot()
    };
  }

  setControls(activityId, handlers = {}) {
    const id = normalizeNumber(activityId);
    if (!id || !this.active.has(id)) {
      return null;
    }
    const safeHandlers = {
      cancel: typeof handlers?.cancel === 'function' ? handlers.cancel : null,
      nextStep: typeof handlers?.nextStep === 'function' ? handlers.nextStep : null
    };
    this.controls.set(id, safeHandlers);
    return this.updateActivity(id, {
      canCancel: Boolean(safeHandlers.cancel),
      canNextStep: Boolean(safeHandlers.nextStep)
    });
  }

  async invokeControl(activityId, control, payload = {}) {
    const id = normalizeNumber(activityId);
    if (!id || !this.active.has(id)) {
      return {
        ok: false,
        code: 'NOT_FOUND',
        message: 'Aktivität nicht gefunden oder bereits abgeschlossen.'
      };
    }
    const handlers = this.controls.get(id) || {};
    const key = control === 'nextStep' ? 'nextStep' : 'cancel';
    const fn = handlers[key];
    if (typeof fn !== 'function') {
      return {
        ok: false,
        code: 'UNSUPPORTED',
        message: key === 'nextStep'
          ? 'Nächster-Schritt ist für diese Aktivität nicht verfügbar.'
          : 'Abbrechen ist für diese Aktivität nicht verfügbar.'
      };
    }
    try {
      const result = await fn(payload);
      return {
        ok: true,
        code: 'OK',
        result: result && typeof result === 'object' ? result : null
      };
    } catch (error) {
      return {
        ok: false,
        code: 'FAILED',
        message: error?.message || 'Aktion fehlgeschlagen.'
      };
    }
  }

  async requestCancel(activityId, payload = {}) {
    return this.invokeControl(activityId, 'cancel', payload);
  }

  async requestNextStep(activityId, payload = {}) {
    return this.invokeControl(activityId, 'nextStep', payload);
  }
}

module.exports = new RuntimeActivityService();
