'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');
const pushStore = require('./mobile-push-store');

const CATEGORY_SEVERITY = Object.freeze({
  waiting: 'high',
  approval: 'high',
  crashed: 'high',
  finished: 'medium',
  walle: 'low',
  walle_repair: 'high',
  test: 'low',
});

function loadWebPush() {
  return require('web-push');
}

function ensureVapidKeys(configDir, options = {}) {
  const webPush = options.webPush || loadWebPush();
  const fsImpl = options.fs || fs;
  const filePath = options.filePath || path.join(configDir, 'vapid.json');
  try {
    const existing = JSON.parse(fsImpl.readFileSync(filePath, 'utf8'));
    if (existing?.publicKey && existing?.privateKey) {
      try { fsImpl.chmodSync(filePath, 0o600); } catch {}
      return { ...existing, filePath };
    }
  } catch {}
  const keys = webPush.generateVAPIDKeys();
  fsImpl.mkdirSync(path.dirname(filePath), { recursive: true });
  fsImpl.writeFileSync(filePath, JSON.stringify(keys, null, 2), { mode: 0o600 });
  try { fsImpl.chmodSync(filePath, 0o600); } catch {}
  return { ...keys, filePath };
}

function sanitizePreview(value, maxLen = 64) {
  return String(value || '')
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, maxLen);
}

function deepLinkFor(notification) {
  if (notification.deepLink) return String(notification.deepLink);
  if (notification.taskId) return `/m/#walle/task/${encodeURIComponent(notification.taskId)}`;
  if (notification.sessionId) return `/m/#agent/${encodeURIComponent(notification.sessionId)}`;
  return '/m/';
}

function normalizeNotification(input = {}, now = Date.now()) {
  const category = String(input.category || '').trim();
  if (!CATEGORY_SEVERITY[category]) throw new Error('notification_category_invalid');
  const sessionId = input.sessionId ? String(input.sessionId) : '';
  const taskId = input.taskId ? String(input.taskId) : '';
  const createdAt = Number(input.createdAt || now);
  const stateKey = String(input.stateKey || input.state || input.reason || createdAt);
  const id = String(input.id || `${category}:${sessionId || taskId || 'global'}:${stateKey}`);
  return {
    id,
    category,
    source: String(input.source || 'ctm'),
    sessionId,
    taskId,
    severity: input.severity || CATEGORY_SEVERITY[category],
    createdAt,
    deepLink: deepLinkFor({ ...input, sessionId, taskId }),
    privatePreview: sanitizePreview(input.privatePreview || input.preview || ''),
    runtimeMs: Number(input.runtimeMs || 0),
  };
}

function titleFor(notification) {
  switch (notification.category) {
    case 'waiting': return 'CTM agent waiting';
    case 'approval': return 'CTM approval needed';
    case 'crashed': return 'CTM agent crashed';
    case 'finished': return 'CTM agent finished';
    case 'walle': return 'Wall-E update';
    case 'walle_repair': return 'Wall-E needs repair';
    case 'test': return 'CTM notifications enabled';
    default: return 'CTM update';
  }
}

function genericBodyFor(notification) {
  switch (notification.category) {
    case 'waiting': return 'An agent is waiting for your response.';
    case 'approval': return 'An agent needs an approval decision.';
    case 'crashed': return 'An agent stopped unexpectedly.';
    case 'finished': return 'An agent finished a long-running task.';
    case 'walle': return 'Wall-E has an update for you.';
    case 'walle_repair': return 'Wall-E needs attention. CTM will keep checking; open CTM for repair details.';
    case 'test': return 'Push delivery is working for this device.';
    default: return 'Open CTM on your phone for details.';
  }
}

function urgencyFor(notification) {
  if (notification.severity === 'high') return 'high';
  if (notification.severity === 'medium') return 'normal';
  return 'low';
}

function payloadFor(notification, settings = {}) {
  const includePreview = !!settings.include_preview && !!notification.privatePreview;
  return {
    id: notification.id,
    category: notification.category,
    severity: notification.severity,
    title: titleFor(notification),
    body: includePreview ? notification.privatePreview : genericBodyFor(notification),
    tag: notification.id,
    renotify: notification.severity === 'high',
    timestamp: notification.createdAt,
    data: {
      deepLink: notification.deepLink,
      sessionId: notification.sessionId || '',
      taskId: notification.taskId || '',
      category: notification.category,
    },
  };
}

class MobileNotificationDispatcher {
  constructor(options = {}) {
    this._db = options.db;
    this._configDir = options.configDir || path.join(os.homedir(), '.walle', 'data');
    this._webPush = options.webPush || loadWebPush();
    this._subject = options.subject || process.env.CTM_VAPID_SUBJECT || 'mailto:ctm@example.invalid';
    this._logger = options.logger || console;
    this._now = options.now || (() => Date.now());
    this._keys = null;
  }

  _getDb() {
    return typeof this._db === 'function' ? this._db() : this._db;
  }

  _ensureSender() {
    if (!this._keys) {
      this._keys = ensureVapidKeys(this._configDir, { webPush: this._webPush });
      if (typeof this._webPush.setVapidDetails === 'function') {
        this._webPush.setVapidDetails(this._subject, this._keys.publicKey, this._keys.privateKey);
      }
    }
    return this._keys;
  }

  getVapidPublicKey() {
    return this._ensureSender().publicKey;
  }

  async dispatch(input, options = {}) {
    const db = this._getDb();
    if (!db) throw new Error('push_db_required');
    pushStore.ensureMobilePushSchema(db);
    this._ensureSender();
    const at = this._now();
    const notification = normalizeNotification(input, at);
    if (!options.force && !pushStore.claimNotification(db, notification, at)) {
      return { ok: true, skipped: 'dedupe', attempted: 0, sent: 0, failed: 0 };
    }
    try { pushStore.cleanupOldDedupe(db, at); } catch {}

    let targets = pushStore.listDeliverableSubscriptions(db, { atMs: at });
    if (options.deviceId) targets = targets.filter((target) => target.deviceId === options.deviceId);

    const result = { ok: true, attempted: 0, sent: 0, failed: 0, skipped: 0, failures: [] };
    for (const target of targets) {
      const settings = target.settings || pushStore.getPushSettings(db, target.deviceId);
      if (notification.category === 'finished' && notification.runtimeMs < settings.finished_min_runtime_ms) {
        result.skipped += 1;
        continue;
      }
      const quiet = pushStore.isQuietNow(settings, new Date(at));
      if (quiet && (notification.severity !== 'high' || settings.quiet_suppresses_high)) {
        result.skipped += 1;
        continue;
      }
      const payload = payloadFor(notification, settings);
      result.attempted += 1;
      try {
        await this._webPush.sendNotification(target.subscription, JSON.stringify(payload), {
          TTL: notification.severity === 'high' ? 60 : 300,
          urgency: urgencyFor(notification),
        });
        pushStore.recordPushSuccess(db, target.id, at);
        result.sent += 1;
      } catch (err) {
        pushStore.recordPushFailure(db, target.id, err, at);
        result.failed += 1;
        result.failures.push({ id: target.id, statusCode: err?.statusCode || err?.status || 0 });
        if (this._logger?.warn) {
          this._logger.warn(`[mobile-push] delivery failed sub=${target.id} status=${err?.statusCode || err?.status || 'unknown'}`);
        }
      }
    }
    return result;
  }

  notifyWaitingInput(event = {}) {
    const createdAt = event.createdAt || this._now();
    return this.dispatch({
      category: 'waiting',
      source: event.source || 'session-state-bus',
      sessionId: event.sessionId || event.id,
      severity: 'high',
      createdAt,
      stateKey: event.stateKey || `waiting:${createdAt}`,
      privatePreview: event.privatePreview || '',
    });
  }

  notifyApproval(event = {}) {
    const createdAt = event.createdAt || this._now();
    return this.dispatch({
      category: 'approval',
      source: event.source || 'approval-agent',
      sessionId: event.sessionId || event.id,
      severity: 'high',
      createdAt,
      stateKey: event.stateKey || `approval:${createdAt}`,
    });
  }

  notifySessionCrashed(event = {}) {
    const createdAt = event.createdAt || this._now();
    return this.dispatch({
      category: 'crashed',
      source: 'session-events',
      sessionId: event.sessionId || event.id,
      severity: 'high',
      createdAt,
      stateKey: event.stateKey || `crashed:${createdAt}:${event.exitCode || ''}`,
    });
  }

  notifySessionFinished(event = {}) {
    const createdAt = event.createdAt || this._now();
    return this.dispatch({
      category: 'finished',
      source: 'session-events',
      sessionId: event.sessionId || event.id,
      severity: 'medium',
      createdAt,
      runtimeMs: Number(event.runtimeMs || 0),
      stateKey: event.stateKey || `finished:${createdAt}:${event.exitCode || 0}`,
    });
  }

  notifyWalleRepairNeeded(event = {}) {
    const createdAt = event.createdAt || this._now();
    return this.dispatch({
      category: 'walle_repair',
      source: event.source || 'walle-repair',
      severity: 'high',
      createdAt,
      deepLink: '/m/#walle',
      stateKey: event.stateKey || `walle_repair:${event.incidentId || event.code || createdAt}`,
    });
  }

  sendTestToDevice(deviceId) {
    return this.dispatch({
      category: 'test',
      source: 'mobile-settings',
      severity: 'low',
      createdAt: this._now(),
      stateKey: `test:${this._now()}`,
    }, { force: true, deviceId });
  }
}

function createMobileNotificationDispatcher(options = {}) {
  return new MobileNotificationDispatcher(options);
}

module.exports = {
  CATEGORY_SEVERITY,
  MobileNotificationDispatcher,
  createMobileNotificationDispatcher,
  ensureVapidKeys,
  normalizeNotification,
  payloadFor,
  sanitizePreview,
};
