import crypto from 'node:crypto';
import type pg from 'pg';
import { pool } from '../db.js';
import { config } from '../config.js';

const PROVIDERS = ['generic', 'dingtalk', 'feishu', 'wecom'] as const;
type NotificationProvider = typeof PROVIDERS[number];

interface NotificationChannelRow {
  id: string;
  channelCode: string;
  name: string;
  provider: NotificationProvider;
  webhookUrl: string;
  secretRef: string | null;
  minSeverity: string;
  statusFilter: string[];
  actionPatterns: string[];
  tenantIds: string[];
  timeoutSec: number | null;
}

interface NotificationEventRow {
  id: string;
  channelId: string;
  channelCode: string;
  channelName: string;
  alertId: string;
  auditLogId: string | null;
  provider: NotificationProvider;
  attempts: number;
  webhookUrl: string;
  secretRef: string | null;
  timeoutSec: number | null;
  tenantId: string | null;
  tenantSlug: string | null;
  tenantName: string | null;
  severity: string;
  alertStatus: string;
  action: string;
  targetType: string | null;
  targetId: string | null;
  title: string;
  summary: string | null;
  details: Record<string, unknown> | null;
  alertCreatedAt: string;
}

interface SecretRow {
  secretValue: string | null;
  secretJson: Record<string, unknown> | null;
}

interface PreparedRequest {
  provider: NotificationProvider;
  url: string;
  body: Record<string, unknown>;
  headers: Record<string, string>;
}

interface SendResult {
  ok: boolean;
  httpCode: number;
  responseSummary: string;
  errorMessage: string;
}

interface ProcessResult {
  enqueued: number;
  processed: number;
  sent: number;
  failed: number;
  retrying: number;
  discarded: number;
}

function objectValue(value: unknown): Record<string, unknown> {
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

function stringValue(value: unknown, fallback = '') {
  return typeof value === 'string' && value.trim() ? value.trim() : fallback;
}

function truncate(value: unknown, max = 1900) {
  return String(value ?? '').slice(0, max);
}

function severityRank(value: string) {
  if (value === 'critical') return 4;
  if (value === 'high') return 3;
  if (value === 'medium') return 2;
  return 1;
}

function redactAuditNotificationValue(value: unknown, parentKey = '', depth = 0): unknown {
  if (value === null || value === undefined) return value;
  if (depth > 8) return '[REDACTED_DEPTH_LIMIT]';
  if (
    /(?:password|passwd|secret|token|credential|private[_-]?key|api[_-]?key|app[_-]?secret|authorization|cookie|session|cert|signature|nonce)$/i
      .test(parentKey)
  ) {
    return '[REDACTED]';
  }
  if (Array.isArray(value)) return value.map(item => redactAuditNotificationValue(item, parentKey, depth + 1));
  if (typeof value === 'object') {
    const output: Record<string, unknown> = {};
    for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
      output[key] = redactAuditNotificationValue(item, key, depth + 1);
    }
    return output;
  }
  return value;
}

function matchesPattern(value: string, patterns: string[]) {
  if (!patterns.length) return true;
  return patterns.some(pattern => (
    value === pattern
    || (pattern.endsWith('*') && value.startsWith(pattern.slice(0, -1)))
  ));
}

function parseSecretRef(ref: string | null) {
  if (!ref) return null;
  const parts = ref.split(':');
  if (parts.length !== 3 || parts[0] !== 'app_private.platform_secrets') return null;
  return { scope: parts[1], key: parts[2] };
}

function secretText(secret: SecretRow | null, keys: string[]) {
  if (!secret) return '';
  if (secret.secretValue?.trim()) return secret.secretValue.trim();
  const json = objectValue(secret.secretJson);
  for (const key of keys) {
    const value = json[key];
    if (typeof value === 'string' && value.trim()) return value.trim();
  }
  return '';
}

function validateWebhookUrl(rawUrl: string) {
  let url: URL;
  try {
    url = new URL(rawUrl);
  } catch {
    throw new Error('Platform audit notification webhook URL is invalid');
  }
  const isLocalhost = ['127.0.0.1', 'localhost', '::1'].includes(url.hostname);
  if (url.protocol !== 'https:' && !(config.platformAuditNotificationAllowInsecureLocalhost && isLocalhost)) {
    throw new Error('Platform audit notification webhook URL must use HTTPS outside local development');
  }
  url.username = '';
  url.password = '';
  return url;
}

function dingtalkSign(secret: string): Record<string, string> {
  if (!secret) return {};
  const timestamp = String(Date.now());
  const sign = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}\n${secret}`)
    .digest('base64');
  return { timestamp, sign };
}

function feishuSign(secret: string): Record<string, string> {
  if (!secret) return {};
  const timestamp = String(Math.floor(Date.now() / 1000));
  const sign = crypto
    .createHmac('sha256', `${timestamp}\n${secret}`)
    .update('')
    .digest('base64');
  return { timestamp, sign };
}

function appendQuery(url: URL, params: Record<string, string>) {
  for (const [key, value] of Object.entries(params)) {
    if (value) url.searchParams.set(key, value);
  }
  return url.toString();
}

function alertPayload(task: NotificationEventRow) {
  return {
    event: 'platform.audit.alert',
    alert: {
      id: task.alertId,
      severity: task.severity,
      status: task.alertStatus,
      title: task.title,
      summary: task.summary,
      action: task.action,
      targetType: task.targetType,
      targetId: task.targetId,
      createdAt: task.alertCreatedAt,
      details: redactAuditNotificationValue(task.details || {}),
    },
    tenant: task.tenantId
      ? {
          id: task.tenantId,
          slug: task.tenantSlug,
          name: task.tenantName,
        }
      : null,
    source: {
      channelId: task.channelId,
      channelCode: task.channelCode,
      auditLogId: task.auditLogId,
    },
  };
}

function alertMarkdown(task: NotificationEventRow) {
  const payload = alertPayload(task);
  const tenant = payload.tenant;
  return [
    `### 平台审计告警：${task.title}`,
    `- 级别：${task.severity}`,
    `- 状态：${task.alertStatus}`,
    `- 租户：${tenant?.name || tenant?.slug || '平台'}`,
    `- 动作：${task.action}`,
    `- 目标：${[task.targetType, task.targetId].filter(Boolean).join(':') || '无'}`,
    `- 摘要：${task.summary || '无'}`,
    `- 告警ID：${task.alertId}`,
    `- 时间：${String(task.alertCreatedAt).slice(0, 19).replace('T', ' ')}`,
  ].join('\n');
}

function prepareRequest(task: NotificationEventRow, secret: SecretRow | null): PreparedRequest {
  const target = validateWebhookUrl(task.webhookUrl);
  const provider = task.provider;
  const secretValue = secretText(secret, ['secret', 'signSecret', 'webhookSecret']);
  const headers = { 'content-type': 'application/json' };
  const markdown = alertMarkdown(task);

  if (provider === 'dingtalk') {
    return {
      provider,
      url: appendQuery(target, dingtalkSign(secretValue)),
      headers,
      body: {
        msgtype: 'markdown',
        markdown: {
          title: '平台审计告警',
          text: markdown,
        },
      },
    };
  }

  if (provider === 'feishu') {
    return {
      provider,
      url: target.toString(),
      headers,
      body: {
        msg_type: 'interactive',
        ...feishuSign(secretValue),
        card: {
          config: { wide_screen_mode: true },
          header: { title: { tag: 'plain_text', content: '平台审计告警' }, template: 'red' },
          elements: [{ tag: 'markdown', content: markdown }],
        },
      },
    };
  }

  if (provider === 'wecom') {
    return {
      provider,
      url: target.toString(),
      headers,
      body: {
        msgtype: 'markdown',
        markdown: { content: markdown },
      },
    };
  }

  return {
    provider,
    url: target.toString(),
    headers,
    body: alertPayload(task),
  };
}

async function loadEnabledChannels(client: pg.PoolClient) {
  const result = await client.query<NotificationChannelRow>(
    `
    select id, channel_code as "channelCode", name, provider,
           webhook_url as "webhookUrl", secret_ref as "secretRef",
           min_severity as "minSeverity", status_filter as "statusFilter",
           action_patterns as "actionPatterns", tenant_ids as "tenantIds",
           timeout_sec as "timeoutSec"
    from public.platform_audit_notification_channels
    where enabled = true
    order by created_at asc
    `,
  );
  return result.rows;
}

async function loadOpenAlerts(client: pg.PoolClient, limit: number) {
  const result = await client.query<{
    id: string;
    auditLogId: string;
    tenantId: string | null;
    severity: string;
    status: string;
    action: string;
  }>(
    `
    select id, audit_log_id as "auditLogId", tenant_id as "tenantId",
           severity, status, action
    from public.platform_audit_alerts
    where status in ('open', 'acknowledged')
    order by
      case severity when 'critical' then 1 when 'high' then 2 when 'medium' then 3 else 4 end,
      created_at asc
    limit $1
    `,
    [limit],
  );
  return result.rows;
}

async function enqueueNotificationEvents(client: pg.PoolClient, limit: number) {
  const channels = await loadEnabledChannels(client);
  if (!channels.length) return 0;
  const alerts = await loadOpenAlerts(client, Math.max(limit * 4, 50));
  let enqueued = 0;

  for (const alert of alerts) {
    for (const channel of channels) {
      if (enqueued >= limit) return enqueued;
      if (!channel.statusFilter.includes(alert.status)) continue;
      if (severityRank(alert.severity) < severityRank(channel.minSeverity)) continue;
      if (!matchesPattern(alert.action, channel.actionPatterns)) continue;
      if (channel.tenantIds.length && (!alert.tenantId || !channel.tenantIds.includes(alert.tenantId))) continue;

      const result = await client.query(
        `
        insert into public.platform_audit_notification_events (
          channel_id, alert_id, audit_log_id, provider, status,
          request_payload, metadata
        )
        values ($1, $2, $3, $4, 'pending', $5::jsonb, $6::jsonb)
        on conflict (channel_id, alert_id) do nothing
        returning id
        `,
        [
          channel.id,
          alert.id,
          alert.auditLogId,
          channel.provider,
          JSON.stringify({
            channelCode: channel.channelCode,
            alertId: alert.id,
            severity: alert.severity,
            action: alert.action,
          }),
          JSON.stringify({ enqueuedBy: 'platform-audit-notifications-worker' }),
        ],
      );
      if (result.rowCount) enqueued += 1;
    }
  }
  return enqueued;
}

async function claimDueEvents(client: pg.PoolClient, limit: number) {
  const result = await client.query<NotificationEventRow>(
    `
    with due as (
      select e.id
      from public.platform_audit_notification_events e
      where e.status in ('pending', 'retrying')
        and coalesce(e.next_attempt_at, e.scheduled_at, e.created_at) <= now()
      order by coalesce(e.next_attempt_at, e.scheduled_at, e.created_at) asc, e.created_at asc
      limit $1
      for update skip locked
    )
    update public.platform_audit_notification_events e
    set status = 'processing',
        last_attempt_at = now(),
        updated_at = now()
    from due
    join public.platform_audit_notification_channels c on true
    join public.platform_audit_alerts a on true
    left join public.tenants t on t.id = a.tenant_id
    where e.id = due.id
      and c.id = e.channel_id
      and a.id = e.alert_id
    returning e.id, e.channel_id as "channelId", c.channel_code as "channelCode",
              c.name as "channelName", e.alert_id as "alertId",
              e.audit_log_id as "auditLogId", e.provider, e.attempts,
              c.webhook_url as "webhookUrl", c.secret_ref as "secretRef",
              c.timeout_sec as "timeoutSec", a.tenant_id as "tenantId",
              t.slug::text as "tenantSlug", t.name as "tenantName",
              a.severity, a.status as "alertStatus", a.action,
              a.target_type as "targetType", a.target_id as "targetId",
              a.title, a.summary, a.details,
              a.created_at as "alertCreatedAt"
    `,
    [limit],
  );
  return result.rows;
}

async function recoverStaleProcessingEvents(client: pg.PoolClient) {
  const staleMs = Math.max(config.platformAuditNotificationRequestTimeoutMs * 3, 60_000);
  await client.query(
    `
    update public.platform_audit_notification_events
    set status = 'retrying',
        next_attempt_at = now(),
        last_error = coalesce(last_error, 'Recovered stale processing platform audit notification'),
        updated_at = now()
    where status = 'processing'
      and coalesce(last_attempt_at, updated_at, created_at) < now() - ($1::int * interval '1 millisecond')
    `,
    [staleMs],
  );
}

async function loadSecret(client: pg.PoolClient, secretRef: string | null) {
  const parsed = parseSecretRef(secretRef);
  if (!parsed) return null;
  const result = await client.query<SecretRow>(
    `
    select secret_value as "secretValue", secret_json as "secretJson"
    from app_private.platform_secrets
    where secret_scope = $1
      and secret_key = $2
    limit 1
    `,
    [parsed.scope, parsed.key],
  );
  return result.rows[0] || null;
}

async function sendWebhook(request: PreparedRequest, timeoutMs: number): Promise<SendResult> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(request.url, {
      method: 'POST',
      headers: request.headers,
      body: JSON.stringify(request.body),
      signal: controller.signal,
    });
    const text = await response.text().catch(() => '');
    return {
      ok: response.ok,
      httpCode: response.status,
      responseSummary: truncate(text, 1900),
      errorMessage: response.ok ? '' : `Platform audit notification webhook returned HTTP ${response.status}`,
    };
  } catch (error) {
    return {
      ok: false,
      httpCode: 0,
      responseSummary: '',
      errorMessage: error instanceof Error ? error.message : String(error),
    };
  } finally {
    clearTimeout(timeout);
  }
}

async function markEventResult(
  client: pg.PoolClient,
  task: NotificationEventRow,
  request: PreparedRequest | null,
  result: SendResult,
  attempt: number,
) {
  const backoff = config.platformAuditNotificationBackoffSeconds[Math.min(attempt - 1, config.platformAuditNotificationBackoffSeconds.length - 1)] || 60;
  const requestPayload = {
    provider: request?.provider || task.provider,
    urlHost: request ? new URL(request.url).hostname : null,
    body: request?.body ? redactAuditNotificationValue(request.body) : null,
  };

  if (result.ok) {
    await client.query(
      `
      update public.platform_audit_notification_events
      set status = 'sent',
          attempts = $2,
          next_attempt_at = null,
          last_error = null,
          last_http_code = $3,
          last_response_summary = $4,
          request_payload = $5::jsonb,
          sent_at = now(),
          updated_at = now()
      where id = $1
      `,
      [task.id, attempt, result.httpCode, truncate(result.responseSummary), JSON.stringify(requestPayload)],
    );
    return 'sent';
  }

  const terminal = attempt >= config.platformAuditNotificationMaxAttempts;
  await client.query(
    `
    update public.platform_audit_notification_events
    set status = $2,
        attempts = $3,
        next_attempt_at = case when $2 = 'retrying' then now() + ($4::int * interval '1 second') else null end,
        last_error = $5,
        last_http_code = $6,
        last_response_summary = $7,
        request_payload = $8::jsonb,
        updated_at = now()
    where id = $1
    `,
    [
      task.id,
      terminal ? 'failed' : 'retrying',
      attempt,
      backoff,
      truncate(result.errorMessage),
      result.httpCode,
      truncate(result.responseSummary),
      JSON.stringify(requestPayload),
    ],
  );
  return terminal ? 'failed' : 'retrying';
}

async function discardEvent(client: pg.PoolClient, task: NotificationEventRow, message: string) {
  await client.query(
    `
    update public.platform_audit_notification_events
    set status = 'discarded',
        attempts = attempts + 1,
        last_attempt_at = now(),
        last_error = $2,
        request_payload = $3::jsonb,
        updated_at = now()
    where id = $1
    `,
    [
      task.id,
      truncate(message),
      JSON.stringify({
        provider: task.provider,
        urlHost: '',
        body: null,
      }),
    ],
  );
  return 'discarded';
}

async function processEvent(task: NotificationEventRow) {
  const client = await pool.connect();
  try {
    if (!PROVIDERS.includes(task.provider)) {
      return discardEvent(client, task, 'Unsupported platform audit notification provider');
    }
    const secret = await loadSecret(client, task.secretRef);
    const request = prepareRequest(task, secret);
    const timeoutMs = Math.max(1000, (task.timeoutSec || 0) * 1000 || config.platformAuditNotificationRequestTimeoutMs);
    const attempt = task.attempts + 1;
    const result = await sendWebhook(request, timeoutMs);
    return markEventResult(client, task, request, result, attempt);
  } catch (error) {
    const attempt = task.attempts + 1;
    return markEventResult(client, task, null, {
      ok: false,
      httpCode: 0,
      responseSummary: '',
      errorMessage: error instanceof Error ? error.message : String(error),
    }, attempt);
  } finally {
    client.release();
  }
}

export async function processPlatformAuditNotificationBatch(limit = config.platformAuditNotificationBatchSize): Promise<ProcessResult> {
  const client = await pool.connect();
  let tasks: NotificationEventRow[] = [];
  let enqueued = 0;
  try {
    await client.query('begin');
    await recoverStaleProcessingEvents(client);
    enqueued = await enqueueNotificationEvents(client, limit);
    tasks = await claimDueEvents(client, limit);
    await client.query('commit');
  } catch (error) {
    await client.query('rollback');
    throw error;
  } finally {
    client.release();
  }

  const result: ProcessResult = { enqueued, processed: tasks.length, sent: 0, failed: 0, retrying: 0, discarded: 0 };
  for (const task of tasks) {
    const status = await processEvent(task);
    if (status === 'sent') result.sent += 1;
    if (status === 'failed') result.failed += 1;
    if (status === 'retrying') result.retrying += 1;
    if (status === 'discarded') result.discarded += 1;
  }
  return result;
}
