export const dynamic = 'force-dynamic';
export const revalidate = 0;

import { NextResponse } from 'next/server';
import { computeSignals } from '../../../lib/signals.js';
import { fireWebhooksForOrg } from '../../../lib/webhooks.js';
import { sendSignalAlertEmail } from '../../../lib/notifications.js';
import { logActivity } from '../../../lib/audit.js';
import { getSql } from '../../../lib/db.js';
import crypto from 'crypto';
import { timingSafeCompare } from '../../../lib/timing-safe.js';
import { publishOrgEvent, EVENTS } from '../../../lib/events.js';

/**
 * Hash a signal into a stable identifier for deduplication.
 * Uses type + relevant IDs to create a unique fingerprint.
 */
function hashSignal(signal) {
  // Include every resource-id-like field that might uniquely distinguish
  // signals of the same type for the same agent. session_stalled carried
  // session_id but it wasn't hashed — so multiple stalled sessions for
  // one agent deduped to a single alert. integration_mismatch carries a
  // provider field (added by F62) with the same collision risk. Keeping
  // all slots unconditionally means adding a new signal type later is
  // one-line: append its id field.
  const parts = [
    signal.type,
    signal.agent_id || '',
    signal.action_id || '',
    signal.loop_id || '',
    signal.assumption_id || '',
    signal.session_id || '',
    signal.provider || '',
  ].join(':');
  // SHA-256 (not MD5): this is a dedup fingerprint, but using a non-broken
  // digest keeps the codebase free of weak-crypto findings. Truncate to 32 hex
  // chars so the stored fingerprint length is unchanged from the MD5 era.
  return crypto.createHash('sha256').update(parts).digest('hex').slice(0, 32);
}

// GET /api/cron/signals - Vercel Cron handler
export async function GET(request) {
  try {
    // SECURITY: Always require CRON_SECRET — no dev bypass
    const cronSecret = process.env.CRON_SECRET;
    if (!cronSecret) {
      return NextResponse.json({ error: 'CRON_SECRET not configured' }, { status: 503 });
    }
    const authHeader = request.headers.get('authorization');
    if (!authHeader || !timingSafeCompare(authHeader, `Bearer ${cronSecret}`)) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const sql = getSql();
    const summary = { orgs_processed: 0, new_signals: 0, emails_sent: 0, webhooks_fired: 0, native_notifications: 0 };

    // Load active orgs
    const orgs = await sql`
      SELECT id, name FROM organizations WHERE id != 'org_default'
    `;

    for (const org of orgs) {
      try {
        const signals = await computeSignals(org.id, null, sql);
        if (signals.length === 0) {
          summary.orgs_processed++;
          continue;
        }

        // Hash each signal
        const currentHashes = signals.map(s => ({ ...s, _hash: hashSignal(s) }));

        // Load existing snapshots for this org
        const existingSnapshots = await sql`
          SELECT signal_hash FROM signal_snapshots WHERE org_id = ${org.id}
        `;
        const existingSet = new Set(existingSnapshots.map(s => s.signal_hash));

        // Find NEW signals (hash not in snapshot)
        const newSignals = currentHashes.filter(s => !existingSet.has(s._hash));

        // Upsert all current signals into snapshots
        const now = new Date().toISOString();
        for (const s of currentHashes) {
          await sql`
            INSERT INTO signal_snapshots (org_id, signal_hash, signal_type, severity, agent_id, first_seen_at, last_seen_at)
            VALUES (${org.id}, ${s._hash}, ${s.type}, ${s.severity}, ${s.agent_id || null}, ${now}, ${now})
            ON CONFLICT (org_id, signal_hash) DO UPDATE SET
              last_seen_at = ${now},
              severity = ${s.severity}
          `;
        }

        if (newSignals.length === 0) {
          summary.orgs_processed++;
          continue;
        }

        summary.new_signals += newSignals.length;

        // Clean _hash before sending
        const cleanSignals = newSignals.map(({ _hash, ...rest }) => rest);

        // Log signal detection
        logActivity({
          orgId: org.id, actorId: 'cron', actorType: 'cron',
          action: 'signal.detected', resourceType: 'signal',
          details: { count: cleanSignals.length, types: [...new Set(cleanSignals.map(s => s.type))] },
        }, sql);

        // Fire webhooks
        const whResults = await fireWebhooksForOrg(org.id, cleanSignals, sql);
        const whFired = whResults.filter(r => r.success).length;
        summary.webhooks_fired += whFired;

        if (whFired > 0) {
          logActivity({
            orgId: org.id, actorId: 'system', actorType: 'system',
            action: 'webhook.fired', resourceType: 'webhook',
            details: { count: whFired, signal_count: cleanSignals.length },
          }, sql);
        }

        // Native notifications via configured integrations
        try {
          const { deliverNativeNotifications } = await import('../../../lib/notification-adapters/index.js');
          const { getSettings } = await import('../../../lib/repositories/settings.repository.js');
          const settings = await getSettings(sql, org.id, { category: 'integration' });
          const nativeResults = await deliverNativeNotifications(org.id, cleanSignals, settings, sql);
          for (const r of nativeResults) {
            if (r.success) {
              summary.native_notifications++;
              logActivity({
                orgId: org.id, actorId: 'system', actorType: 'system',
                action: `notification.${r.provider}.sent`, resourceType: 'notification',
                details: { provider: r.provider, signals: cleanSignals.length, message: r.message },
              }, sql);
            }
          }
        } catch (nativeErr) {
          console.error(`[CRON] Native notification error for ${org.id}:`, nativeErr.message);
        }

        // Publish SSE events for realtime UI updates
        for (const signal of cleanSignals) {
          void publishOrgEvent(EVENTS.SIGNAL_DETECTED, {
            orgId: org.id,
            signal,
          });
        }

        // Send email alerts to opted-in users
        const prefs = await sql`
          SELECT np.user_id, np.signal_types, u.email
          FROM notification_preferences np
          JOIN users u ON np.user_id = u.id
          WHERE np.org_id = ${org.id}
            AND np.channel = 'email'
            AND np.enabled = 1
            AND u.org_id = ${org.id}
        `;

        for (const pref of prefs) {
          let subscribedTypes;
          try {
            subscribedTypes = JSON.parse(pref.signal_types);
          } catch {
            subscribedTypes = ['all'];
          }

          const relevantSignals = subscribedTypes.includes('all')
            ? cleanSignals
            : cleanSignals.filter(s => subscribedTypes.includes(s.type));

          if (relevantSignals.length === 0) continue;

          const sent = await sendSignalAlertEmail(pref.email, org.name, relevantSignals);
          if (sent) {
            summary.emails_sent++;
            logActivity({
              orgId: org.id, actorId: 'system', actorType: 'system',
              action: 'alert.email_sent', resourceType: 'signal',
              details: { to: pref.email, signal_count: relevantSignals.length },
            }, sql);
          }
        }

        summary.orgs_processed++;
      } catch (err) {
        console.error(`[CRON] Error processing org ${org.id}:`, err.message);
        summary.orgs_processed++;
      }
    }

    return NextResponse.json({ success: true, summary });
  } catch (error) {
    console.error('Cron signals error:', error);
    return NextResponse.json({ error: 'Cron job failed' }, { status: 500 });
  }
}
