'use strict';

const crypto = require('crypto');
const os = require('os');

const DEFAULT_RELAY_URL = 'https://relay.walle.sh';
const DEFAULT_MOBILE_URL = 'https://m.walle.sh';

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

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

function safeJsonParse(raw, fallback = null) {
  try {
    return raw ? JSON.parse(raw) : fallback;
  } catch {
    return fallback;
  }
}

function normalizeUrl(value, fallback) {
  const raw = String(value || '').trim();
  if (!raw) return fallback;
  try {
    const u = new URL(raw);
    if (u.protocol !== 'https:' && u.protocol !== 'http:') return fallback;
    return u.toString().replace(/\/+$/, '');
  } catch {
    return fallback;
  }
}

function ensureRemoteRelaySchema(db) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS ctm_remote_instances (
      id TEXT PRIMARY KEY,
      display_name TEXT NOT NULL,
      relay_account_id TEXT,
      relay_url TEXT NOT NULL,
      mobile_url TEXT NOT NULL,
      public_key TEXT NOT NULL,
      created_at INTEGER NOT NULL,
      last_connected_at INTEGER,
      enabled INTEGER NOT NULL DEFAULT 1
    );

    CREATE TABLE IF NOT EXISTS ctm_remote_message_idempotency (
      message_id TEXT PRIMARY KEY,
      device_id TEXT,
      message_type TEXT NOT NULL,
      result_json TEXT NOT NULL,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS ctm_remote_audit_log (
      id TEXT PRIMARY KEY,
      account_id TEXT,
      instance_id TEXT,
      device_id TEXT,
      session_id TEXT,
      action TEXT NOT NULL,
      risk TEXT NOT NULL,
      result TEXT NOT NULL,
      metadata_json TEXT,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS ctm_remote_submissions (
      message_id TEXT PRIMARY KEY,
      session_id TEXT NOT NULL,
      device_id TEXT,
      message_type TEXT NOT NULL,
      text_hash TEXT NOT NULL,
      text_preview TEXT,
      status TEXT NOT NULL,
      result_json TEXT,
      attempts INTEGER NOT NULL DEFAULT 0,
      created_at INTEGER NOT NULL,
      updated_at INTEGER NOT NULL,
      accepted_at INTEGER,
      expires_at INTEGER,
      last_error TEXT
    );

    CREATE INDEX IF NOT EXISTS idx_ctm_remote_audit_created ON ctm_remote_audit_log(created_at DESC);
    CREATE INDEX IF NOT EXISTS idx_ctm_remote_audit_device ON ctm_remote_audit_log(device_id, created_at DESC);
    CREATE INDEX IF NOT EXISTS idx_ctm_remote_submissions_session ON ctm_remote_submissions(session_id, updated_at DESC);
    CREATE INDEX IF NOT EXISTS idx_ctm_remote_submissions_status ON ctm_remote_submissions(status, updated_at DESC);
  `);
}

function instanceFromRow(row) {
  if (!row) return null;
  return {
    id: row.id,
    display_name: row.display_name,
    relay_account_id: row.relay_account_id || '',
    relay_url: row.relay_url,
    mobile_url: row.mobile_url,
    public_key: row.public_key,
    created_at: row.created_at,
    last_connected_at: row.last_connected_at || null,
    enabled: !!row.enabled,
  };
}

function defaultDisplayName() {
  const host = os.hostname();
  return host ? `${host} CTM` : 'CTM on this Mac';
}

function getOrCreateInstance(db, options = {}) {
  ensureRemoteRelaySchema(db);
  const existing = db.prepare('SELECT * FROM ctm_remote_instances ORDER BY created_at ASC LIMIT 1').get();
  if (existing) return instanceFromRow(existing);

  const createdAt = nowMs();
  const publicKey = crypto.createHash('sha256')
    .update(`${createdAt}:${crypto.randomBytes(32).toString('base64url')}`)
    .digest('base64url');
  const row = {
    id: options.id || randomId('ctm'),
    display_name: String(options.displayName || options.display_name || defaultDisplayName()).trim() || defaultDisplayName(),
    relay_account_id: String(options.relayAccountId || options.relay_account_id || ''),
    relay_url: normalizeUrl(options.relayUrl || options.relay_url || process.env.WALLE_RELAY_URL, DEFAULT_RELAY_URL),
    mobile_url: normalizeUrl(options.mobileUrl || options.mobile_url || process.env.WALLE_MOBILE_URL, DEFAULT_MOBILE_URL),
    public_key: publicKey,
    created_at: createdAt,
    last_connected_at: null,
    enabled: options.enabled === false ? 0 : 1,
  };
  db.prepare(`
    INSERT INTO ctm_remote_instances
      (id, display_name, relay_account_id, relay_url, mobile_url, public_key, created_at, last_connected_at, enabled)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    row.id,
    row.display_name,
    row.relay_account_id,
    row.relay_url,
    row.mobile_url,
    row.public_key,
    row.created_at,
    row.last_connected_at,
    row.enabled,
  );
  return instanceFromRow(row);
}

function updateInstance(db, patch = {}) {
  ensureRemoteRelaySchema(db);
  const current = getOrCreateInstance(db);
  const next = {
    ...current,
    display_name: String(patch.displayName || patch.display_name || current.display_name).trim() || current.display_name,
    relay_account_id: String(patch.relayAccountId || patch.relay_account_id || current.relay_account_id || ''),
    relay_url: normalizeUrl(patch.relayUrl || patch.relay_url || current.relay_url, DEFAULT_RELAY_URL),
    mobile_url: normalizeUrl(patch.mobileUrl || patch.mobile_url || current.mobile_url, DEFAULT_MOBILE_URL),
    enabled: patch.enabled == null ? current.enabled : !!patch.enabled,
  };
  db.prepare(`
    UPDATE ctm_remote_instances
    SET display_name = ?, relay_account_id = ?, relay_url = ?, mobile_url = ?, enabled = ?
    WHERE id = ?
  `).run(next.display_name, next.relay_account_id, next.relay_url, next.mobile_url, next.enabled ? 1 : 0, current.id);
  return getOrCreateInstance(db);
}

function markConnected(db, instanceId, atMs = nowMs()) {
  ensureRemoteRelaySchema(db);
  db.prepare('UPDATE ctm_remote_instances SET last_connected_at = ? WHERE id = ?').run(atMs, String(instanceId || ''));
  return db.prepare('SELECT * FROM ctm_remote_instances WHERE id = ?').get(String(instanceId || ''));
}

function getIdempotentResult(db, messageId) {
  ensureRemoteRelaySchema(db);
  const row = db.prepare('SELECT * FROM ctm_remote_message_idempotency WHERE message_id = ?').get(String(messageId || ''));
  if (!row) return null;
  return {
    message_id: row.message_id,
    device_id: row.device_id || '',
    message_type: row.message_type,
    result: safeJsonParse(row.result_json, null),
    created_at: row.created_at,
  };
}

function storeIdempotentResult(db, message, result, atMs = nowMs()) {
  ensureRemoteRelaySchema(db);
  db.prepare(`
    INSERT OR REPLACE INTO ctm_remote_message_idempotency
      (message_id, device_id, message_type, result_json, created_at)
    VALUES (?, ?, ?, ?, ?)
  `).run(
    String(message.id || ''),
    String(message.device_id || message.deviceId || ''),
    String(message.type || ''),
    JSON.stringify(result || {}),
    atMs,
  );
  return result;
}

function insertAudit(db, entry = {}) {
  ensureRemoteRelaySchema(db);
  const id = entry.id || randomId('remote_audit');
  db.prepare(`
    INSERT INTO ctm_remote_audit_log
      (id, account_id, instance_id, device_id, session_id, action, risk, result, metadata_json, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    id,
    entry.accountId || entry.account_id || '',
    entry.instanceId || entry.instance_id || '',
    entry.deviceId || entry.device_id || '',
    entry.sessionId || entry.session_id || '',
    String(entry.action || ''),
    String(entry.risk || 'low'),
    String(entry.result || 'unknown'),
    entry.metadata ? JSON.stringify(entry.metadata) : '',
    entry.createdAt || entry.created_at || nowMs(),
  );
  return id;
}

function listAudit(db, options = {}) {
  ensureRemoteRelaySchema(db);
  const limit = Math.max(1, Math.min(500, Number(options.limit || 100)));
  const rows = db.prepare('SELECT * FROM ctm_remote_audit_log ORDER BY created_at DESC LIMIT ?').all(limit);
  return rows.map((row) => ({
    ...row,
    metadata: safeJsonParse(row.metadata_json, null),
  }));
}

function submissionTextHash(text) {
  return crypto.createHash('sha256').update(String(text || '')).digest('hex').slice(0, 32);
}

function submissionTextPreview(text, limit = 240) {
  return String(text || '').replace(/\s+/g, ' ').trim().slice(0, Math.max(16, Number(limit) || 240));
}

function submissionFromRow(row) {
  if (!row) return null;
  return {
    message_id: row.message_id,
    session_id: row.session_id,
    device_id: row.device_id || '',
    message_type: row.message_type,
    text_hash: row.text_hash,
    text_preview: row.text_preview || '',
    status: row.status,
    result: safeJsonParse(row.result_json, null),
    attempts: Number(row.attempts || 0) || 0,
    created_at: row.created_at,
    updated_at: row.updated_at,
    accepted_at: row.accepted_at || null,
    expires_at: row.expires_at || null,
    last_error: row.last_error || '',
  };
}

function upsertRemoteSubmission(db, entry = {}, atMs = nowMs()) {
  ensureRemoteRelaySchema(db);
  const messageId = String(entry.messageId || entry.message_id || '').trim();
  const sessionId = String(entry.sessionId || entry.session_id || '').trim();
  const messageType = String(entry.messageType || entry.message_type || '').trim();
  if (!messageId || !sessionId || !messageType) return null;
  const text = String(entry.text || '');
  const textHash = String(entry.textHash || entry.text_hash || submissionTextHash(text));
  const textPreview = String(entry.textPreview || entry.text_preview || submissionTextPreview(text));
  const status = String(entry.status || 'received');
  const resultJson = JSON.stringify(entry.result || {});
  const attempts = Math.max(0, Number(entry.attempts || 0) || 0);
  const expiresAt = entry.expiresAt || entry.expires_at || null;
  db.prepare(`
    INSERT INTO ctm_remote_submissions
      (message_id, session_id, device_id, message_type, text_hash, text_preview, status, result_json, attempts, created_at, updated_at, accepted_at, expires_at, last_error)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(message_id) DO UPDATE SET
      session_id = excluded.session_id,
      device_id = excluded.device_id,
      message_type = excluded.message_type,
      text_hash = excluded.text_hash,
      text_preview = excluded.text_preview,
      status = excluded.status,
      result_json = excluded.result_json,
      attempts = excluded.attempts,
      updated_at = excluded.updated_at,
      accepted_at = COALESCE(excluded.accepted_at, ctm_remote_submissions.accepted_at),
      expires_at = excluded.expires_at,
      last_error = excluded.last_error
  `).run(
    messageId,
    sessionId,
    String(entry.deviceId || entry.device_id || ''),
    messageType,
    textHash,
    textPreview,
    status,
    resultJson,
    attempts,
    entry.createdAt || entry.created_at || atMs,
    atMs,
    entry.acceptedAt || entry.accepted_at || null,
    expiresAt,
    String(entry.lastError || entry.last_error || ''),
  );
  return getRemoteSubmission(db, messageId);
}

function updateRemoteSubmission(db, messageId, patch = {}, atMs = nowMs()) {
  ensureRemoteRelaySchema(db);
  const id = String(messageId || '').trim();
  if (!id) return null;
  const existing = getRemoteSubmission(db, id);
  if (!existing) return null;
  const next = {
    status: String(patch.status || existing.status),
    result: patch.result == null ? (existing.result || {}) : patch.result,
    attempts: patch.attempts == null ? existing.attempts : Math.max(0, Number(patch.attempts || 0) || 0),
    accepted_at: patch.acceptedAt || patch.accepted_at || existing.accepted_at || null,
    expires_at: patch.expiresAt || patch.expires_at || existing.expires_at || null,
    last_error: patch.lastError || patch.last_error || existing.last_error || '',
  };
  db.prepare(`
    UPDATE ctm_remote_submissions
    SET status = ?, result_json = ?, attempts = ?, updated_at = ?, accepted_at = ?, expires_at = ?, last_error = ?
    WHERE message_id = ?
  `).run(
    next.status,
    JSON.stringify(next.result || {}),
    next.attempts,
    atMs,
    next.accepted_at,
    next.expires_at,
    next.last_error,
    id,
  );
  return getRemoteSubmission(db, id);
}

function getRemoteSubmission(db, messageId) {
  ensureRemoteRelaySchema(db);
  return submissionFromRow(db.prepare('SELECT * FROM ctm_remote_submissions WHERE message_id = ?').get(String(messageId || '')));
}

function listRemoteSubmissions(db, options = {}) {
  ensureRemoteRelaySchema(db);
  const limit = Math.max(1, Math.min(200, Number(options.limit || 50)));
  const sessionId = String(options.sessionId || options.session_id || '').trim();
  const sinceMs = Math.max(0, Number(options.sinceMs || options.since_ms || 0) || 0);
  const rows = sessionId
    ? db.prepare(`
        SELECT * FROM ctm_remote_submissions
        WHERE session_id = ? AND updated_at >= ?
        ORDER BY updated_at DESC
        LIMIT ?
      `).all(sessionId, sinceMs, limit)
    : db.prepare(`
        SELECT * FROM ctm_remote_submissions
        WHERE updated_at >= ?
        ORDER BY updated_at DESC
        LIMIT ?
      `).all(sinceMs, limit);
  return rows.map(submissionFromRow).filter(Boolean);
}

function buildRemoteStatus(db, options = {}) {
  const instance = getOrCreateInstance(db, options.instance || {});
  const relayUrl = normalizeUrl(options.relayUrl || instance.relay_url, DEFAULT_RELAY_URL);
  const mobileUrl = normalizeUrl(options.mobileUrl || instance.mobile_url, DEFAULT_MOBILE_URL);
  const connected = !!(options.connected || process.env.WALLE_RELAY_CONNECTED === '1');
  const configured = !!(instance.enabled && relayUrl && mobileUrl);
  return {
    ok: true,
    mode: 'walle_relay',
    recommended: true,
    configured,
    connected,
    instance: {
      ...instance,
      relay_url: relayUrl,
      mobile_url: mobileUrl,
    },
    relay: {
      url: relayUrl,
      mobile_url: mobileUrl,
      connected,
      status: connected ? 'connected' : 'local_ready',
      outbound_only: true,
    },
    capabilities: {
      local_pairing: true,
      hosted_pairing: false,
      typed_messages: true,
      generic_proxy: false,
      high_risk_remote_actions: false,
    },
  };
}

module.exports = {
  DEFAULT_MOBILE_URL,
  DEFAULT_RELAY_URL,
  buildRemoteStatus,
  ensureRemoteRelaySchema,
  getIdempotentResult,
  getOrCreateInstance,
  getRemoteSubmission,
  insertAudit,
  listAudit,
  listRemoteSubmissions,
  markConnected,
  randomId,
  storeIdempotentResult,
  submissionTextHash,
  submissionTextPreview,
  updateInstance,
  updateRemoteSubmission,
  upsertRemoteSubmission,
};
