import componentApi from '@blocklet/sdk/lib/util/component-api';
import { AxiosError } from 'axios';
import { Op } from 'sequelize';

import { wallet } from '../libs/auth';
import logger from '../libs/logger';
import createQueue from '../libs/queue';
import { MAX_RETRY_COUNT, getNextRetry, getWebhookJobId } from '../libs/util';
import { webhookAlertWindowMinutes, webhookAlertMinFailures } from '../libs/env';
import { Customer } from '../store/models/customer';
import { Event } from '../store/models/event';
import { PaymentCurrency } from '../store/models/payment-currency';
import { WebhookAttempt } from '../store/models/webhook-attempt';
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
import { addNotificationJob } from './notification';

async function decrementPendingWebhooks(event: Event, webhookId: string) {
  const existingSuccessCount = await WebhookAttempt.count({
    where: {
      event_id: event.id,
      webhook_endpoint_id: webhookId,
      status: 'succeeded',
    },
  });

  if (existingSuccessCount === 0) {
    if (event.pending_webhooks > 0) {
      await event.decrement('pending_webhooks');
      logger.info('pending_webhooks decremented', { eventId: event.id, newCount: event.pending_webhooks, webhookId });
    } else {
      logger.warn('Attempted to decrement pending_webhooks below 0', {
        eventId: event.id,
        currentCount: event.pending_webhooks,
        webhookId,
      });
    }
  }
}

type WebhookJob = {
  eventId: string;
  webhookId: string;
};

// https://stripe.com/docs/webhooks
export const handleWebhook = async (job: WebhookJob) => {
  logger.info('handle webhook', job);

  const event = await Event.findByPk(job.eventId);
  if (!event) {
    logger.warn('event not found when attempt webhook', job);
    return;
  }

  const webhook = await WebhookEndpoint.findByPk(job.webhookId);
  if (!webhook) {
    logger.warn('webhook not found on attempt', job);
    return;
  }
  if (webhook.status !== 'enabled') {
    logger.warn('webhook disabled on attempt', job);
    return;
  }

  const lastRetryCount = await WebhookAttempt.max('retry_count', {
    where: { event_id: event.id, webhook_endpoint_id: webhook.id },
  });

  const retryCount = lastRetryCount ? +lastRetryCount + 1 : 1;

  try {
    const json = event.toJSON();

    // expand basic fields
    const { object } = json.data;
    if (object.customer_id && !object.customer) {
      object.customer = await Customer.findByPk(object.customer_id);
    }
    if (object.currency_id && !object.currency) {
      object.currency = await PaymentCurrency.findByPk(object.currency_id);
    }

    // verify similar to component call, but supports external urls
    const result = await componentApi.request({
      url: webhook.url,
      method: 'POST',
      timeout: 60 * 1000,
      data: json,
      headers: {
        'x-app-id': wallet.address,
        'x-app-pk': wallet.publicKey,
      },
    });

    await WebhookAttempt.create({
      livemode: event.livemode,
      event_id: event.id,
      webhook_endpoint_id: webhook.id,
      status: 'succeeded',
      response_status: result.status,
      response_body: result.data || {},
      retry_count: retryCount,
    });
    logger.info('WebhookAttempt created successfully', { eventId: event.id, webhookId: webhook.id });

    await decrementPendingWebhooks(event, webhook.id);

    logger.info('webhook attempt success', { ...job, retryCount });
  } catch (err: any) {
    logger.warn('webhook attempt error', { ...job, retryCount, message: err.message });
    const errorStatus = (err as AxiosError).response?.status || 500;

    await WebhookAttempt.create({
      livemode: event.livemode,
      event_id: event.id,
      webhook_endpoint_id: webhook.id,
      status: 'failed',
      response_status: errorStatus,
      response_body: (err as AxiosError).response?.data || {},
      retry_count: retryCount,
    });
    logger.info('Failed WebhookAttempt created', { eventId: event.id, webhookId: webhook.id });

    try {
      await checkAndNotifyWebhookFailures(webhook.id);
    } catch (notifyError: any) {
      logger.error('Failed to check and notify webhook failures', {
        webhookId: webhook.id,
        error: notifyError.message,
      });
    }

    if (retryCount < MAX_RETRY_COUNT) {
      process.nextTick(() => {
        addWebhookJob(event.id, webhook.id, {
          runAt: getNextRetry(retryCount, event.created_at),
          // persist=true: on Cloudflare the queue shim only writes DELAYED jobs to
          // D1 (with will_run_at, dispatched by the cron) when persisted, and does
          // NOT send them to CF Queue. With persist:false the retry was silently
          // dropped, so after the first failure the whole retry ladder vanished
          // and the delivery was lost (PR #1381 review P1).
          persist: true,
        });
      });
    } else {
      await decrementPendingWebhooks(event, webhook.id);
    }
  }
};

// Alert if webhook continuously fails within configured time window
async function checkAndNotifyWebhookFailures(webhookId: string) {
  const alertWindowStart = new Date(Date.now() - webhookAlertWindowMinutes * 60 * 1000);

  const recentAttempts = await WebhookAttempt.findAll({
    where: {
      webhook_endpoint_id: webhookId,
      created_at: { [Op.gte]: alertWindowStart },
    },
    order: [['created_at', 'DESC']],
    attributes: ['status'],
  });

  const hasSuccessInWindow = recentAttempts.some((attempt) => attempt.status === 'succeeded');
  if (hasSuccessInWindow) {
    return;
  }

  const failedCountInWindow = recentAttempts.filter((attempt) => attempt.status === 'failed').length;
  if (failedCountInWindow < webhookAlertMinFailures) {
    return;
  }

  addNotificationJob(
    'webhook.endpoint.failed',
    {
      webhookEndpointId: webhookId,
      failedCount: failedCountInWindow,
    },
    [webhookId],
    true,
    24 * 3600
  );

  logger.info('Notification job added for webhook consecutive failures', {
    webhookId,
    failedCountInWindow,
    alertWindowMinutes: webhookAlertWindowMinutes,
    minFailures: webhookAlertMinFailures,
  });
}

export const webhookQueue = createQueue<WebhookJob>({
  name: 'webhook',
  onJob: handleWebhook,
  options: {
    concurrency: 5,
    maxRetries: 0,
    enableScheduledJob: true,
  },
});

webhookQueue.on('failed', ({ id, job, error }) => {
  logger.error('webhook job failed', { id, job, error });
});

export async function addWebhookJob(
  eventId: string,
  webhookId: string,
  options: {
    runAt?: number;
    persist?: boolean;
    replace?: boolean;
  } = {}
) {
  // Default persist=true so CF Workers' Queue transport is backed by a D1
  // jobs row — if CF Queue is momentarily unavailable or the consumer
  // doesn't pick the message up, the row stays for the next cron dispatcher
  // to retry. With persist=false the message can simply vanish (we lost
  // ~13 minutes of webhook deliveries on staging confirming this).
  const { runAt, persist = true, replace = true } = options;
  const jobId = getWebhookJobId(eventId, webhookId);
  const exist = await webhookQueue.get(jobId);

  if (exist && !replace) {
    logger.info('Webhook job already exists, skipping', { eventId, webhookId, jobId });
    return false;
  }

  if (exist) {
    await webhookQueue.delete(jobId);
  }

  webhookQueue.push({
    id: jobId,
    job: { eventId, webhookId },
    runAt,
    persist,
  });

  logger.info('Webhook job added to queue', { eventId, webhookId, jobId, runAt, persist });
  return true;
}
