'use strict';

const crypto = require('crypto');
const authStore = require('./mobile-auth-store');

const DEFAULT_FINISHED_MIN_RUNTIME_MS = 5 * 60 * 1000;

function nowMs() {
  return Date.now();
}

function randomId(prefix, bytes = 12) {
  return `${prefix}_${crypto.randomBytes(bytes).toString('base64url')}`;
}

function ensureMobilePushSchema(db) {
  authStore.ensureMobileAuthSchema(db);
  db.exec(`
    CREATE TABLE IF NOT EXISTS ctm_push_subs (
      id TEXT PRIMARY KEY,
      device_token_id TEXT NOT NULL REFERENCES ctm_device_tokens(id) ON DELETE CASCADE,
      endpoint TEXT NOT NULL UNIQUE,
      p256dh TEXT NOT NULL,
      auth TEXT NOT NULL,
      user_agent TEXT,
      created_at INTEGER NOT NULL,
      last_seen_at INTEGER NOT NULL,
      last_delivery INTEGER,
      failure_count INTEGER NOT NULL DEFAULT 0,
      disabled_at INTEGER
    );

    CREATE TABLE IF NOT EXISTS ctm_push_settings (
      device_token_id TEXT PRIMARY KEY REFERENCES ctm_device_tokens(id) ON DELETE CASCADE,
      enabled INTEGER NOT NULL DEFAULT 1,
      include_preview INTEGER NOT NULL DEFAULT 0,
      quiet_start TEXT NOT NULL DEFAULT '',
      quiet_end TEXT NOT NULL DEFAULT '',
      quiet_suppresses_high INTEGER NOT NULL DEFAULT 0,
      finished_min_runtime_ms INTEGER NOT NULL DEFAULT ${DEFAULT_FINISHED_MIN_RUNTIME_MS},
      updated_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS ctm_push_dedupe (
      id TEXT PRIMARY KEY,
      category TEXT NOT NULL,
      session_id TEXT,
      created_at INTEGER NOT NULL,
      last_sent_at INTEGER NOT NULL
    );

    CREATE INDEX IF NOT EXISTS idx_ctm_push_subs_device ON ctm_push_subs(device_token_id);
    CREATE INDEX IF NOT EXISTS idx_ctm_push_subs_disabled ON ctm_push_subs(disabled_at);
    CREATE INDEX IF NOT EXISTS idx_ctm_push_dedupe_created ON ctm_push_dedupe(created_at);
  `);
}

function normalizeSubscription(raw) {
  const sub = raw?.subscription || raw || {};
  const endpoint = String(sub.endpoint || '').trim();
  if (!endpoint || endpoint.length > 4096) throw new Error('push_endpoint_required');
  let parsed;
  try { parsed = new URL(endpoint); } catch { throw new Error('push_endpoint_invalid'); }
  if (parsed.protocol !== 'https:') throw new Error('push_endpoint_https_required');
  const keys = sub.keys || {};
  const p256dh = String(keys.p256dh || sub.p256dh || '').trim();
  const auth = String(keys.auth || sub.auth || '').trim();
  if (!p256dh || !auth) throw new Error('push_keys_required');
  if (p256dh.length > 4096 || auth.length > 1024) throw new Error('push_keys_too_large');
  return { endpoint, keys: { p256dh, auth } };
}

function publicSubscription(row) {
  if (!row) return null;
  return {
    id: row.id,
    device_token_id: row.device_token_id,
    endpoint: row.endpoint,
    created_at: row.created_at,
    last_seen_at: row.last_seen_at,
    last_delivery: row.last_delivery || null,
    failure_count: Number(row.failure_count || 0),
    disabled_at: row.disabled_at || null,
  };
}

function defaultSettings(deviceId) {
  return {
    device_token_id: deviceId,
    enabled: true,
    include_preview: false,
    quiet_start: '',
    quiet_end: '',
    quiet_suppresses_high: false,
    finished_min_runtime_ms: DEFAULT_FINISHED_MIN_RUNTIME_MS,
    updated_at: 0,
  };
}

function settingsFromRow(deviceId, row) {
  if (!row) return defaultSettings(deviceId);
  return {
    device_token_id: deviceId,
    enabled: !!row.enabled,
    include_preview: !!row.include_preview,
    quiet_start: row.quiet_start || '',
    quiet_end: row.quiet_end || '',
    quiet_suppresses_high: !!row.quiet_suppresses_high,
    finished_min_runtime_ms: Number(row.finished_min_runtime_ms || DEFAULT_FINISHED_MIN_RUNTIME_MS),
    updated_at: Number(row.updated_at || 0),
  };
}

function getPushSettings(db, deviceId) {
  ensureMobilePushSchema(db);
  const id = String(deviceId || '');
  const row = db.prepare('SELECT * FROM ctm_push_settings WHERE device_token_id = ?').get(id);
  return settingsFromRow(id, row);
}

function validTime(value) {
  return value === '' || /^([01]\d|2[0-3]):[0-5]\d$/.test(value);
}

function normalizeRuntimeMs(value) {
  const runtime = Number(value);
  if (!Number.isFinite(runtime)) throw new Error('finished_runtime_invalid');
  return Math.max(0, Math.min(24 * 60 * 60 * 1000, runtime));
}

function updatePushSettings(db, deviceId, patch = {}) {
  ensureMobilePushSchema(db);
  const id = String(deviceId || '');
  if (!id) throw new Error('device_token_required');
  const current = getPushSettings(db, id);
  const next = { ...current };
  if (Object.prototype.hasOwnProperty.call(patch, 'enabled')) next.enabled = !!patch.enabled;
  if (Object.prototype.hasOwnProperty.call(patch, 'include_preview')) next.include_preview = !!patch.include_preview;
  if (Object.prototype.hasOwnProperty.call(patch, 'includePreview')) next.include_preview = !!patch.includePreview;
  if (Object.prototype.hasOwnProperty.call(patch, 'quiet_start')) next.quiet_start = String(patch.quiet_start || '').slice(0, 5);
  if (Object.prototype.hasOwnProperty.call(patch, 'quietStart')) next.quiet_start = String(patch.quietStart || '').slice(0, 5);
  if (Object.prototype.hasOwnProperty.call(patch, 'quiet_end')) next.quiet_end = String(patch.quiet_end || '').slice(0, 5);
  if (Object.prototype.hasOwnProperty.call(patch, 'quietEnd')) next.quiet_end = String(patch.quietEnd || '').slice(0, 5);
  if (Object.prototype.hasOwnProperty.call(patch, 'quiet_suppresses_high')) next.quiet_suppresses_high = !!patch.quiet_suppresses_high;
  if (Object.prototype.hasOwnProperty.call(patch, 'quietSuppressesHigh')) next.quiet_suppresses_high = !!patch.quietSuppressesHigh;
  if (Object.prototype.hasOwnProperty.call(patch, 'finished_min_runtime_ms')) {
    next.finished_min_runtime_ms = normalizeRuntimeMs(patch.finished_min_runtime_ms);
  }
  if (Object.prototype.hasOwnProperty.call(patch, 'finishedMinRuntimeMs')) {
    next.finished_min_runtime_ms = normalizeRuntimeMs(patch.finishedMinRuntimeMs);
  }
  if (!validTime(next.quiet_start) || !validTime(next.quiet_end)) throw new Error('quiet_hours_invalid');
  next.updated_at = nowMs();
  db.prepare(`
    INSERT INTO ctm_push_settings
      (device_token_id, enabled, include_preview, quiet_start, quiet_end, quiet_suppresses_high, finished_min_runtime_ms, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(device_token_id) DO UPDATE SET
      enabled = excluded.enabled,
      include_preview = excluded.include_preview,
      quiet_start = excluded.quiet_start,
      quiet_end = excluded.quiet_end,
      quiet_suppresses_high = excluded.quiet_suppresses_high,
      finished_min_runtime_ms = excluded.finished_min_runtime_ms,
      updated_at = excluded.updated_at
  `).run(
    id,
    next.enabled ? 1 : 0,
    next.include_preview ? 1 : 0,
    next.quiet_start,
    next.quiet_end,
    next.quiet_suppresses_high ? 1 : 0,
    next.finished_min_runtime_ms,
    next.updated_at,
  );
  return getPushSettings(db, id);
}

function upsertPushSubscription(db, deviceId, raw, context = {}) {
  ensureMobilePushSchema(db);
  const id = String(deviceId || '');
  if (!id) throw new Error('device_token_required');
  const sub = normalizeSubscription(raw);
  const at = nowMs();
  const existing = db.prepare('SELECT id FROM ctm_push_subs WHERE endpoint = ?').get(sub.endpoint);
  const rowId = existing?.id || randomId('push');
  db.prepare(`
    INSERT INTO ctm_push_subs
      (id, device_token_id, endpoint, p256dh, auth, user_agent, created_at, last_seen_at, failure_count, disabled_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)
    ON CONFLICT(endpoint) DO UPDATE SET
      device_token_id = excluded.device_token_id,
      p256dh = excluded.p256dh,
      auth = excluded.auth,
      user_agent = excluded.user_agent,
      last_seen_at = excluded.last_seen_at,
      failure_count = 0,
      disabled_at = NULL
  `).run(
    rowId,
    id,
    sub.endpoint,
    sub.keys.p256dh,
    sub.keys.auth,
    String(context.userAgent || '').slice(0, 500),
    at,
    at,
  );
  updatePushSettings(db, id, { enabled: true });
  return publicSubscription(db.prepare('SELECT * FROM ctm_push_subs WHERE id = ?').get(rowId));
}

function deletePushSubscription(db, deviceId, options = {}) {
  ensureMobilePushSchema(db);
  const id = String(deviceId || '');
  if (!id) throw new Error('device_token_required');
  const subId = String(options.id || '').trim();
  const endpoint = String(options.endpoint || '').trim();
  if (subId) {
    return db.prepare('DELETE FROM ctm_push_subs WHERE id = ? AND device_token_id = ?').run(subId, id).changes;
  }
  if (endpoint) {
    return db.prepare('DELETE FROM ctm_push_subs WHERE endpoint = ? AND device_token_id = ?').run(endpoint, id).changes;
  }
  return db.prepare('DELETE FROM ctm_push_subs WHERE device_token_id = ?').run(id).changes;
}

function listDeviceSubscriptions(db, deviceId) {
  ensureMobilePushSchema(db);
  return db.prepare('SELECT * FROM ctm_push_subs WHERE device_token_id = ? ORDER BY created_at DESC')
    .all(String(deviceId || ''))
    .map(publicSubscription);
}

function subscriptionForWebPush(row) {
  return {
    endpoint: row.endpoint,
    keys: {
      p256dh: row.p256dh,
      auth: row.auth,
    },
  };
}

function listDeliverableSubscriptions(db, options = {}) {
  ensureMobilePushSchema(db);
  const at = Number(options.atMs || nowMs());
  const rows = db.prepare(`
    SELECT s.*, d.label AS device_label, d.scopes AS device_scopes, d.revoked_at, d.expires_at
    FROM ctm_push_subs s
    JOIN ctm_device_tokens d ON d.id = s.device_token_id
    WHERE s.disabled_at IS NULL
      AND d.revoked_at IS NULL
      AND (d.expires_at IS NULL OR d.expires_at > ?)
    ORDER BY s.created_at ASC
  `).all(at);
  return rows.filter((row) => {
    const scopes = authStore.parseScopes(row.device_scopes);
    return scopes.includes('respond') || scopes.includes('admin');
  }).map((row) => {
    const settings = getPushSettings(db, row.device_token_id);
    return {
      id: row.id,
      deviceId: row.device_token_id,
      deviceLabel: row.device_label || 'Phone',
      endpoint: row.endpoint,
      subscription: subscriptionForWebPush(row),
      settings,
    };
  }).filter((row) => row.settings.enabled);
}

function recordPushSuccess(db, subscriptionId, atMs = nowMs()) {
  ensureMobilePushSchema(db);
  return db.prepare(`
    UPDATE ctm_push_subs
    SET last_delivery = ?, failure_count = 0
    WHERE id = ?
  `).run(atMs, String(subscriptionId || ''));
}

function recordPushFailure(db, subscriptionId, error = {}, atMs = nowMs()) {
  ensureMobilePushSchema(db);
  const status = Number(error.statusCode || error.status || 0);
  const disable = status === 404 || status === 410;
  const row = db.prepare('SELECT failure_count FROM ctm_push_subs WHERE id = ?').get(String(subscriptionId || ''));
  const nextFailures = Number(row?.failure_count || 0) + 1;
  const disabledAt = disable || nextFailures >= 10 ? atMs : null;
  return db.prepare(`
    UPDATE ctm_push_subs
    SET failure_count = ?, disabled_at = COALESCE(disabled_at, ?)
    WHERE id = ?
  `).run(nextFailures, disabledAt, String(subscriptionId || ''));
}

function minutesOfDay(hhmm) {
  const [h, m] = String(hhmm || '').split(':').map(Number);
  if (!Number.isFinite(h) || !Number.isFinite(m)) return null;
  return h * 60 + m;
}

function isQuietNow(settings, date = new Date()) {
  const start = minutesOfDay(settings?.quiet_start);
  const end = minutesOfDay(settings?.quiet_end);
  if (start == null || end == null || start === end) return false;
  const now = date.getHours() * 60 + date.getMinutes();
  if (start < end) return now >= start && now < end;
  return now >= start || now < end;
}

function claimNotification(db, notification, atMs = nowMs()) {
  ensureMobilePushSchema(db);
  const id = String(notification?.id || '');
  if (!id) throw new Error('notification_id_required');
  const result = db.prepare(`
    INSERT OR IGNORE INTO ctm_push_dedupe (id, category, session_id, created_at, last_sent_at)
    VALUES (?, ?, ?, ?, ?)
  `).run(id, String(notification.category || ''), notification.sessionId || null, atMs, atMs);
  return result.changes > 0;
}

function cleanupOldDedupe(db, atMs = nowMs(), ttlMs = 7 * 24 * 60 * 60 * 1000) {
  ensureMobilePushSchema(db);
  return db.prepare('DELETE FROM ctm_push_dedupe WHERE created_at < ?').run(atMs - ttlMs);
}

module.exports = {
  DEFAULT_FINISHED_MIN_RUNTIME_MS,
  cleanupOldDedupe,
  claimNotification,
  deletePushSubscription,
  ensureMobilePushSchema,
  getPushSettings,
  isQuietNow,
  listDeliverableSubscriptions,
  listDeviceSubscriptions,
  normalizeSubscription,
  recordPushFailure,
  recordPushSuccess,
  updatePushSettings,
  upsertPushSubscription,
};
