import { Prisma } from '@prisma/client';
import {
  BadRequestException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { getAppConfig } from '../config/env';
import { AuthenticatedUser } from '../auth/auth.types';
import { ConversationsService } from '../conversations/conversations.service';
import { JobsService } from '../jobs/jobs.service';
import { defaultWhatsappSubscriptions } from '../integrations/whatsapp-subscriptions';
import { normalizePhoneNumber } from '../common/normalize';
import { PrismaService } from '../prisma/prisma.service';
import {
  normalizeWebhookPayload,
  verifyMetaSignature,
} from './webhooks.utils';
import type {
  NormalizedWebhookEvent,
  WebhookHeaders,
  WebhookReceiveSummary,
} from './webhooks.types';

@Injectable()
export class WebhooksService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly jobsService: JobsService,
    private readonly conversationsService: ConversationsService,
  ) {}

  async verifyChallenge(mode?: string, token?: string, challenge?: string) {
    const config = await this.getEffectiveWhatsappConfig();

    if (mode !== 'subscribe') {
      throw new BadRequestException('Unsupported webhook verification mode');
    }

    if ((token || '') !== config.webhookVerifyToken) {
      throw new UnauthorizedException('Invalid webhook verify token');
    }

    return challenge || '';
  }

  async receive(
    provider: string,
    payload: unknown,
    headers: WebhookHeaders,
    rawBody?: Buffer,
  ): Promise<WebhookReceiveSummary> {
    const verification = await this.verifyWebhookRequest(provider, rawBody, headers);
    const normalizedEvents = normalizeWebhookPayload(provider, payload);

    let queued = 0;
    let duplicates = 0;
    let ignored = 0;
    const queuedEventIds: string[] = [];
    const config = await this.getEffectiveWhatsappConfig();

    for (const event of normalizedEvents) {
      const outcome = await this.persistNormalizedEvent(
        event,
        verification.verified,
        config.subscriptions.includes(event.eventType),
      );
      if (outcome === 'queued') {
        queued += 1;
        queuedEventIds.push(event.eventId);
      } else if (outcome === 'duplicate') {
        duplicates += 1;
      } else {
        ignored += 1;
      }
    }

    return {
      provider: provider.toLowerCase(),
      verified: verification.verified,
      verification: verification.reason,
      received: normalizedEvents.length,
      queued,
      duplicates,
      ignored,
      eventIds: normalizedEvents.map((event) => event.eventId),
    };
  }

  findAll(limit = 50, provider?: string, status?: string) {
    return this.prisma.webhookEvent.findMany({
      where: {
        provider: provider?.trim() || undefined,
        processingStatus: status?.trim() || undefined,
      },
      orderBy: { createdAt: 'desc' },
      take: Math.min(Math.max(limit, 1), 100),
    });
  }

  async retryEvent(eventId: string, user?: AuthenticatedUser, ipAddress?: string) {
    const event = await this.prisma.webhookEvent.findUnique({
      where: { eventId },
    });

    if (!event) {
      throw new NotFoundException('Webhook event not found');
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.webhookEvent.update({
        where: { eventId },
        data: {
          processingStatus: 'queued',
          processingNotes: 'Manually requeued from retry endpoint',
        },
      });
    });

    await this.jobsService.enqueue({
      queueName: 'webhooks',
      jobType: 'webhook.process',
      payload: { eventId },
      maxAttempts: 3,
    });

    if (user) {
      const actor = await this.prisma.user.findUnique({
        where: { id: user.sub },
        select: { id: true, name: true, email: true },
      });
      await this.prisma.auditLog.create({
        data: {
          actorUserId: actor?.id || user.sub,
          actorName: actor?.name || user.email,
          actorEmail: actor?.email || user.email,
          actionType: 'Webhook Retry Queued',
          module: 'Webhooks',
          ipAddress: ipAddress || null,
          severity: 'default',
          details: `Manually requeued webhook event ${eventId}.`,
        },
      });
    }

    return {
      eventId,
      status: 'queued',
    };
  }

  async processJob(jobId: string) {
    const job = await this.jobsService.findById(jobId);
    if (!job) {
      return;
    }

    try {
      const payload = job.payloadJson as { eventId?: string };
      const eventId = payload?.eventId;

      if (!eventId) {
        throw new Error('Webhook job payload is missing eventId');
      }

      await this.processSingleEvent(eventId);
      await this.jobsService.complete(jobId);
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown processing error';

      if (job.attempts < job.maxAttempts) {
        await this.jobsService.retry(jobId, message, 2000 * job.attempts);
      } else {
        await this.jobsService.fail(jobId, message);
      }
    }
  }

  private async persistNormalizedEvent(
    event: NormalizedWebhookEvent,
    verified: boolean,
    isSubscribed: boolean,
  ) {
    try {
      await this.prisma.$transaction(async (tx) => {
        await tx.webhookEvent.create({
          data: {
            provider: event.provider,
            eventId: event.eventId,
            eventType: event.eventType,
            senderPhone: event.senderPhone,
            recipientPhone: event.recipientPhone,
            externalMessageId: event.externalMessageId,
            eventTimestamp: event.eventTimestamp,
            payloadJson: event.payload as Prisma.InputJsonValue,
            verified,
            processingStatus: isSubscribed ? 'queued' : 'ignored',
            processingNotes: isSubscribed
              ? 'Queued for webhook worker'
              : 'Ignored because event subscription is disabled',
          },
        });
      });

      if (!isSubscribed) {
        return 'ignored' as const;
      }

      await this.jobsService.enqueue({
        queueName: 'webhooks',
        jobType: 'webhook.process',
        payload: { eventId: event.eventId },
        maxAttempts: 3,
      });

      return 'queued' as const;
    } catch (error) {
      if (
        error instanceof Prisma.PrismaClientKnownRequestError &&
        error.code === 'P2002'
      ) {
        return 'duplicate' as const;
      }

      throw error;
    }
  }

  private async verifyWebhookRequest(
    provider: string,
    rawBody: Buffer | undefined,
    headers: WebhookHeaders,
  ) {
    const config = await this.getEffectiveWhatsappConfig();
    const normalizedProvider = provider.toLowerCase();
    const metaSignature = this.readHeader(headers['x-hub-signature-256']);
    const genericSecret = this.readHeader(headers['x-webhook-secret']);

    if (normalizedProvider === 'meta' && config.appSecret) {
      if (!rawBody || !metaSignature) {
        throw new UnauthorizedException('Missing meta webhook signature');
      }

      verifyMetaSignature(rawBody, metaSignature, config.appSecret);
      return { verified: true, reason: 'meta-signature' };
    }

    if (genericSecret) {
      if (genericSecret !== config.sharedSecret) {
        throw new UnauthorizedException('Invalid webhook shared secret');
      }

      return { verified: true, reason: 'shared-secret' };
    }

    if (config.allowUnsigned) {
      return { verified: false, reason: 'unsigned-development-request' };
    }

    throw new UnauthorizedException('Webhook request could not be verified');
  }

  private async getEffectiveWhatsappConfig() {
    const env = getAppConfig();
    const stored = await this.prisma.integrationConfig.findUnique({
      where: { configKey: 'whatsapp' },
    });
    const storedJson = (stored?.configJson as Record<string, unknown> | null) ?? {};

    return {
      webhookVerifyToken:
        typeof storedJson.webhookVerifyToken === 'string'
          ? storedJson.webhookVerifyToken
          : env.webhookVerifyToken,
      sharedSecret:
        typeof storedJson.sharedSecret === 'string'
          ? storedJson.sharedSecret
          : env.webhookSharedSecret,
      appSecret:
        typeof storedJson.appSecret === 'string'
          ? storedJson.appSecret
          : env.metaWebhookAppSecret,
      allowUnsigned: env.webhookAllowUnsigned,
      subscriptions:
        Array.isArray(storedJson.subscriptions) && storedJson.subscriptions.length > 0
          ? storedJson.subscriptions.filter((item): item is string => typeof item === 'string')
          : defaultWhatsappSubscriptions,
    };
  }

  private readHeader(value: string | string[] | undefined) {
    if (Array.isArray(value)) {
      return value[0];
    }

    return value;
  }

  private async processSingleEvent(eventId: string) {
    const event = await this.prisma.webhookEvent.findUnique({
      where: { eventId },
    });

    if (!event) {
      throw new Error(`Webhook event ${eventId} not found`);
    }

    if (event.processingStatus === 'processed') {
      return;
    }

    if (event.eventType === 'message.inbound' && event.senderPhone) {
      const normalizedPhone = normalizePhoneNumber(event.senderPhone);
      const contact = await this.prisma.contact.upsert({
        where: { phoneNumber: normalizedPhone },
        update: {},
        create: {
          name: normalizedPhone,
          phoneNumber: normalizedPhone,
          notes: `Created from inbound webhook event ${event.eventId}`,
        },
      });

      await this.conversationsService.syncInboundFromWebhookEvent({
        webhookEventId: event.eventId,
        contactId: contact.id,
        externalMessageId: event.externalMessageId,
        body: this.extractMessageBody(event.payloadJson),
        occurredAt: event.eventTimestamp,
      });
    }

    if (
      (event.eventType === 'message.sent' || event.eventType === 'message.delivered' || event.eventType === 'message.read' || event.eventType === 'message.failed')
      && event.externalMessageId
    ) {
      await this.prisma.conversationMessage.updateMany({
        where: {
          externalMessageId: event.externalMessageId,
        },
        data: {
          status:
            event.eventType === 'message.read'
              ? 'read'
              : event.eventType === 'message.delivered'
                ? 'delivered'
                : event.eventType === 'message.failed'
                  ? 'failed'
                  : 'sent',
          ...(event.eventType === 'message.read' ? { readAt: event.eventTimestamp } : {}),
        },
      });
    }

    await this.prisma.webhookEvent.update({
      where: { eventId },
      data: {
        processingStatus: 'processed',
        processingNotes:
          event.eventType === 'message.inbound' && event.senderPhone
            ? 'Processed and synced inbound sender into contacts'
            : 'Processed by webhook worker',
      },
    });
  }

  private extractMessageBody(payload: unknown) {
    if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
      return 'Inbound message received.';
    }

    const record = payload as Record<string, unknown>;
    const textRecord =
      record.text && typeof record.text === 'object' && !Array.isArray(record.text)
        ? (record.text as Record<string, unknown>)
        : null;
    const interactiveRecord =
      record.interactive && typeof record.interactive === 'object' && !Array.isArray(record.interactive)
        ? (record.interactive as Record<string, unknown>)
        : null;

    return (
      [
        typeof textRecord?.body === 'string' ? textRecord.body : null,
        typeof record.body === 'string' ? record.body : null,
        typeof record.caption === 'string' ? record.caption : null,
        typeof interactiveRecord?.title === 'string' ? interactiveRecord.title : null,
        typeof record.type === 'string' ? `[${record.type}]` : null,
      ].find((value) => typeof value === 'string' && value.trim()) || 'Inbound message received.'
    );
  }
}
