import crypto from "node:crypto";
import { NextRequest, NextResponse } from "next/server";

import {
  ConversationStatus,
  DeliveryStatus,
  MessageDirection,
  MessageType,
  OptInStatus
} from "@prisma/client";

import { prisma } from "@/lib/prisma";
import { getRequestAuditContext, writeAuditTrail } from "@/lib/audit";
import { recalculateCampaignTotals } from "@/lib/campaign-utils";
import { consumeRateLimit, getRateLimitHeaders } from "@/lib/rate-limit";

type JsonRecord = Record<string, unknown>;

type NormalizedEvent = {
  eventType: string;
  tenantId: string;
  channelId?: string;
  channelPhoneNumberId?: string;
  providerEventId?: string | null;
  payload: JsonRecord;
  rawDirection: "inbound" | "status" | "other";
  inbound?: {
    from: string;
    body?: string;
    contactName?: string;
    messageId?: string | null;
  };
  status?: {
    messageId?: string | null;
    deliveryStatus: "sent" | "delivered" | "read" | "failed" | string;
    failureReason?: string;
  };
};

type WebhookProcessStatus = "processed" | "failed" | "skipped";

function getString(value: unknown) {
  if (typeof value === "string") {
    return value.trim();
  }

  return "";
}

function normalizePhone(value: string) {
  return value.replace(/\D/g, "");
}

function resolveNumber(raw: string | undefined, fallback: number) {
  const parsed = Number(raw?.trim());
  if (!Number.isInteger(parsed) || parsed <= 0) {
    return fallback;
  }

  return parsed;
}

function getWebhookIp(req: NextRequest) {
  const forwarded = req.headers.get("x-forwarded-for");
  return (forwarded ? forwarded.split(",")[0]?.trim() : null)
    || req.headers.get("x-real-ip")
    || "unknown";
}

function buildWebhookEventHash(event: NormalizedEvent, resolvedChannelId: string) {
  const inboundBody = event.inbound?.body?.trim();
  const statusDelivery = event.status?.deliveryStatus?.trim();
  const peerPhone =
    event.inbound?.from ||
    event.channelPhoneNumberId ||
    event.providerEventId ||
    null;
  const payload = {
    tenantId: event.tenantId,
    eventType: event.eventType,
    channelId: resolvedChannelId,
    providerEventId: event.providerEventId?.trim() || null,
    direction: event.rawDirection,
    messageId: event.status?.messageId || event.inbound?.messageId || null,
    peerPhone,
    bodyHash: inboundBody ? crypto.createHash("sha256").update(inboundBody).digest("hex") : null,
    deliveryStatus: statusDelivery || null,
    failureReason: event.status?.failureReason?.trim() || null
  };

  return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
}

async function isDuplicateWebhookEvent(tenantId: string, channelId: string, eventHash: string) {
  const existing = await prisma.webhookEvent.findFirst({
    where: {
      tenantId,
      channelId,
      eventHash,
      processStatus: {
        in: ["processed", "skipped"]
      }
    },
    select: { id: true }
  });

  return Boolean(existing);
}

async function writeWebhookEvent(params: {
  tenantId: string;
  channelId: string | null;
  event: NormalizedEvent;
  processStatus: WebhookProcessStatus;
  eventHash: string;
  failedReason?: string;
}) {
  const now = new Date();
  await prisma.webhookEvent.create({
    data: {
      tenantId: params.tenantId,
      channelId: params.channelId ?? null,
      eventType: params.event.eventType,
      providerEventId: params.event.providerEventId,
      payloadJson: JSON.stringify(params.event.payload),
      processStatus: params.processStatus,
      failedReason: params.failedReason,
      eventHash: params.eventHash,
      processedAt: params.processStatus !== "failed" ? now : null
    }
  });
}

function getStatusDelivery(status: string): DeliveryStatus {
  const normalized = status.toLowerCase();
  if (normalized === "sent") {
    return DeliveryStatus.SENT;
  }

  if (normalized === "delivered") {
    return DeliveryStatus.DELIVERED;
  }

  if (normalized === "read") {
    return DeliveryStatus.READ;
  }

  if (normalized === "accepted" || normalized === "accepted_by_sms" || normalized === "pending") {
    return DeliveryStatus.SENT;
  }

  if (normalized === "undelivered") {
    return DeliveryStatus.FAILED;
  }

  if (normalized === "failed") {
    return DeliveryStatus.FAILED;
  }

  return DeliveryStatus.QUEUED;
}

function shouldAdvanceDeliveryStatus(currentStatus: DeliveryStatus, nextStatus: DeliveryStatus) {
  const score: Record<DeliveryStatus, number> = {
    [DeliveryStatus.QUEUED]: 1,
    [DeliveryStatus.SENT]: 2,
    [DeliveryStatus.DELIVERED]: 3,
    [DeliveryStatus.READ]: 4,
    [DeliveryStatus.FAILED]: 0
  };

  if (nextStatus === DeliveryStatus.READ) {
    return DeliveryStatus.READ;
  }

  if (nextStatus === DeliveryStatus.DELIVERED && currentStatus === DeliveryStatus.READ) {
    return DeliveryStatus.READ;
  }

  if (nextStatus === DeliveryStatus.FAILED && (currentStatus === DeliveryStatus.DELIVERED || currentStatus === DeliveryStatus.READ)) {
    return currentStatus;
  }

  if (nextStatus === DeliveryStatus.FAILED && (currentStatus === DeliveryStatus.FAILED || currentStatus === DeliveryStatus.SENT || currentStatus === DeliveryStatus.QUEUED)) {
    return nextStatus;
  }

  if (currentStatus === DeliveryStatus.FAILED && nextStatus !== DeliveryStatus.DELIVERED) {
    return currentStatus;
  }

  if (score[nextStatus] > score[currentStatus]) {
    return nextStatus;
  }

  return currentStatus;
}

function verifyMetaSignature(rawBody: string, signatureHeader: string | null) {
  const secret = process.env.WHATSAPP_WEBHOOK_SECRET?.trim();
  if (!secret) {
    if (process.env.NODE_ENV === "production") {
      return false;
    }
    return true;
  }

  if (!signatureHeader) {
    return false;
  }

  const split = signatureHeader.split("=");
  if (split.length !== 2 || split[0] !== "sha256") {
    return false;
  }

  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const provided = split[1];
  if (provided.length !== expected.length) {
    return false;
  }

  const expectedBytes = Buffer.from(expected, "hex");
  const providedBytes = Buffer.from(provided, "hex");
  return crypto.timingSafeEqual(expectedBytes, providedBytes);
}

function parseMetaPayload(payload: JsonRecord) {
  const tenantId = getString(payload.tenantId) || getString(payload.tenant_id);
  const out: NormalizedEvent[] = [];
  const entry = payload.entry;

  if (!Array.isArray(entry)) {
    return out;
  }

  for (const entryItem of entry) {
    const rawChanges = (entryItem as JsonRecord).changes;
    const changes = Array.isArray(rawChanges) ? (rawChanges as unknown[]) : [];

    for (const rawChange of changes) {
      const change = rawChange as JsonRecord;
      const value = change.value as JsonRecord | undefined;
      if (!value || typeof value !== "object") {
        continue;
      }

      const metadata = value.metadata as JsonRecord | undefined;
      const phoneNumberId = getString(metadata?.phone_number_id || metadata?.phoneNumberId);
      const messages = Array.isArray(value.messages) ? value.messages : [];
      const statuses = Array.isArray(value.statuses) ? value.statuses : [];

      for (const rawMessage of messages) {
        const message = rawMessage as JsonRecord;
        const messageId = getString(message.id);
        const from = normalizePhone(getString(message.from));
        const text = (message.text as JsonRecord | undefined)?.body;
        const body = getString(text);

        if (!from || !tenantId) {
          continue;
        }

        const contacts = Array.isArray(value.contacts) ? value.contacts : [];
        const matchedContact = contacts.find((item) => getString((item as JsonRecord).wa_id) === from) as JsonRecord | undefined;
        const rawProfile = typeof matchedContact?.profile === "object" ? (matchedContact.profile as JsonRecord | null) : null;
        const contactName = getString(rawProfile?.name) || from;

        out.push({
          eventType: "message.inbound",
          tenantId,
          channelPhoneNumberId: phoneNumberId || undefined,
          providerEventId: messageId || undefined,
          payload,
          rawDirection: "inbound",
          inbound: {
            from,
            body,
            contactName,
            messageId: messageId || null
          }
        });
      }

      for (const rawStatus of statuses) {
        const statusValue = rawStatus as JsonRecord;
        const status = getString(statusValue.status);
        const messageId = getString(statusValue.id);
        const to = normalizePhone(getString(statusValue.recipient_id || statusValue.to));

        if (!tenantId || !messageId) {
          continue;
        }

        out.push({
          eventType: "message.status",
          tenantId,
          channelPhoneNumberId: phoneNumberId || to || undefined,
          providerEventId: messageId,
          payload,
          rawDirection: "status",
          status: {
            messageId,
            deliveryStatus: status,
            failureReason: getString((statusValue.errors as unknown as JsonRecord[])?.[0]?.title)
          }
        });
      }
    }
  }

  return out;
}

function parseLegacyPayload(payload: JsonRecord) {
  const out: NormalizedEvent[] = [];
  const tenantId = getString(payload.tenantId || payload.tenant_id);
  const eventType = getString(payload.eventType || payload.type || payload.event_type || payload.event);

  if (!tenantId || !eventType) {
    return out;
  }

  if (eventType.includes("status")) {
    out.push({
      eventType,
      tenantId,
      channelId: getString(payload.channelId || payload.channel_id) || undefined,
      channelPhoneNumberId: getString(payload.channelPhoneNumberId || payload.phoneNumberId || payload.phone_number_id) || undefined,
      providerEventId: getString(payload.providerEventId || payload.eventId || payload.id),
      payload,
      rawDirection: "status",
      status: {
        messageId: getString(payload.messageId || payload.message_id),
        deliveryStatus: getString(payload.status || "failed"),
        failureReason: getString(payload.failureReason || payload.failedReason)
      }
    });
    return out;
  }

  if (eventType.includes("inbound") || eventType.includes("message")) {
    out.push({
      eventType,
      tenantId,
      channelId: getString(payload.channelId || payload.channel_id) || undefined,
      channelPhoneNumberId: getString(payload.channelPhoneNumberId || payload.phoneNumberId || payload.phone_number_id) || undefined,
      providerEventId: getString(payload.providerMessageId || payload.messageId || payload.id),
      payload,
      rawDirection: "inbound",
      inbound: {
        from: normalizePhone(getString(payload.from || payload.phone || payload.recipient)),
        body: getString(payload.body || (payload.message as JsonRecord)?.body || payload.text),
        contactName: getString(payload.contactName || payload.name),
        messageId: getString(payload.providerMessageId || payload.messageId || payload.id)
      }
    });
  }

  return out;
}

function mapEventDirection(event: NormalizedEvent) {
  if (event.rawDirection === "other") {
    return "other";
  }

  if (event.rawDirection === "status" || event.rawDirection === "inbound") {
    return event.rawDirection;
  }

  return "other";
}

async function resolveChannelId(tenantId: string, channelId?: string, phoneNumberId?: string) {
  if (channelId) {
    const channel = await prisma.channel.findFirst({ where: { id: channelId, tenantId } });
    return channel?.id ?? null;
  }

  if (phoneNumberId) {
    const channel = await prisma.channel.findFirst({ where: { phoneNumberId, tenantId } });
    if (channel) {
      return channel.id;
    }
  }

  return null;
}

export async function GET(req: NextRequest) {
  const rate = consumeRateLimit(getWebhookIp(req), {
    scope: "whatsapp_webhook_get",
    limit: resolveNumber(process.env.WHATSAPP_WEBHOOK_RATE_LIMIT_GET, 60),
    windowMs: resolveNumber(process.env.WHATSAPP_WEBHOOK_RATE_LIMIT_WINDOW_MS, 60 * 1000)
  });

  if (!rate.allowed) {
    return NextResponse.json(
      { ok: false, error: "Too many webhook verification requests" },
      {
        status: 429,
        headers: getRateLimitHeaders(rate)
      }
    );
  }

  const verifyToken = process.env.WHATSAPP_WEBHOOK_VERIFY_TOKEN?.trim() || "";
  const mode = req.nextUrl.searchParams.get("hub.mode");
  const token = req.nextUrl.searchParams.get("hub.verify_token");
  const challenge = req.nextUrl.searchParams.get("hub.challenge");

  if (mode === "subscribe" && token === verifyToken && challenge) {
    return new NextResponse(challenge, { status: 200 });
  }

  return NextResponse.json({ ok: false, error: "Invalid verification request" }, { status: 403 });
}

export async function POST(req: NextRequest) {
  const rate = consumeRateLimit(getWebhookIp(req), {
    scope: "whatsapp_webhook_post",
    limit: resolveNumber(process.env.WHATSAPP_WEBHOOK_RATE_LIMIT_POST, 120),
    windowMs: resolveNumber(process.env.WHATSAPP_WEBHOOK_RATE_LIMIT_WINDOW_MS, 60 * 1000)
  });

  if (!rate.allowed) {
    return NextResponse.json(
      { ok: false, error: "Too many webhook events" },
      {
        status: 429,
        headers: getRateLimitHeaders(rate)
      }
    );
  }

  const raw = await req.text();

  if (!verifyMetaSignature(raw, req.headers.get("x-hub-signature-256"))) {
    return NextResponse.json({ ok: false, error: "Invalid webhook signature" }, { status: 401 });
  }

  let payload: unknown;
  try {
    payload = JSON.parse(raw);
  } catch {
    return NextResponse.json({ ok: false, error: "Invalid JSON payload" }, { status: 400 });
  }

  if (!payload || typeof payload !== "object") {
    return NextResponse.json({ ok: false, error: "Payload must be a JSON object" }, { status: 400 });
  }

  const payloadObj = payload as JsonRecord;
  const parsedEvents = [...parseMetaPayload(payloadObj), ...parseLegacyPayload(payloadObj)];
  if (parsedEvents.length === 0) {
    return NextResponse.json({ ok: true, processed: 0, skipped: 0 });
  }

  let processed = 0;
  let failed = 0;
  let skipped = 0;

    for (const event of parsedEvents) {
      const direction = mapEventDirection(event);
      const now = new Date();

      const resolvedChannelId = await resolveChannelId(event.tenantId, event.channelId, event.channelPhoneNumberId);
      if (!resolvedChannelId) {
        const eventHash = buildWebhookEventHash(event, event.channelPhoneNumberId || event.channelId || "unresolved");
        await writeWebhookEvent({
          tenantId: event.tenantId,
          channelId: null,
          event,
          eventHash,
          processStatus: "failed",
          failedReason: "Channel not found"
        });
        failed += 1;
        continue;
      }

    const eventHash = buildWebhookEventHash(event, resolvedChannelId);
    if (await isDuplicateWebhookEvent(event.tenantId, resolvedChannelId, eventHash)) {
      await writeWebhookEvent({
        tenantId: event.tenantId,
        channelId: resolvedChannelId,
        event,
        eventHash,
        processStatus: "skipped"
      });
      skipped += 1;
      continue;
    }

    if (direction === "inbound") {
      const inbound = event.inbound;
      if (!inbound) {
        failed += 1;
        await writeWebhookEvent({
          tenantId: event.tenantId,
          channelId: resolvedChannelId,
          event,
          eventHash,
          processStatus: "failed",
          failedReason: "Invalid inbound payload"
        });
        continue;
      }

      const fromPhone = inbound.from;
      if (!fromPhone) {
        failed += 1;
        await writeWebhookEvent({
          tenantId: event.tenantId,
          channelId: resolvedChannelId,
          event,
          eventHash,
          processStatus: "failed",
          failedReason: "Missing sender phone number"
        });
        continue;
      }

      const contact = await prisma.contact.upsert({
        where: {
          tenantId_phoneNumber: {
            tenantId: event.tenantId,
            phoneNumber: fromPhone
          }
        },
        create: {
          tenantId: event.tenantId,
          channelId: resolvedChannelId,
          fullName: inbound.contactName || fromPhone,
          phoneNumber: fromPhone,
          optInStatus: OptInStatus.OPTED_IN
        },
        update: {
          channelId: resolvedChannelId,
          fullName: inbound.contactName || fromPhone
        }
      });

      let conversation = await prisma.conversation.findFirst({
        where: {
          tenantId: event.tenantId,
          channelId: resolvedChannelId,
          contactId: contact.id
        },
        orderBy: { lastMessageAt: "desc" }
      });

      if (!conversation) {
        conversation = await prisma.conversation.create({
          data: {
            tenantId: event.tenantId,
            channelId: resolvedChannelId,
            contactId: contact.id,
            subject: inbound.body?.slice(0, 80) ?? "WhatsApp inbound",
            firstMessageAt: now,
            lastMessageAt: now,
            lastInboundAt: now,
            status: ConversationStatus.OPEN
          }
        });
      }

      const existingInbound = inbound.messageId
        ? await prisma.message.findUnique({
            where: { providerMessageId: inbound.messageId }
          })
        : null;

      if (!inbound.messageId || !existingInbound) {
        await prisma.message.create({
          data: {
            tenantId: event.tenantId,
            conversationId: conversation.id,
            channelId: resolvedChannelId,
            contactId: contact.id,
            direction: MessageDirection.INBOUND,
            type: MessageType.TEXT,
            providerMessageId: inbound.messageId,
            contentText: inbound.body,
            sentAt: now,
            sentByUserId: null
          }
        });
      }

      await prisma.conversation.update({
        where: { id: conversation.id },
        data: {
          lastMessageAt: now,
          lastInboundAt: now,
          status: ConversationStatus.OPEN
        }
      });

      await prisma.contact.update({
        where: { id: contact.id },
        data: { lastInteractionAt: now }
      });

      await prisma.conversationActivity.create({
        data: {
          tenantId: event.tenantId,
          conversationId: conversation.id,
          actorUserId: null,
          activityType: "MESSAGE_RECEIVED",
          metadataJson: JSON.stringify({
            provider: "webhook",
            messageId: inbound.messageId,
            body: inbound.body?.slice(0, 120)
          })
        }
      });

      await writeWebhookEvent({
        tenantId: event.tenantId,
        channelId: resolvedChannelId,
        event,
        eventHash,
        processStatus: "processed"
      });

      await prisma.channel.update({
        where: { id: resolvedChannelId },
        data: { webhookStatus: "healthy", lastSyncAt: now }
      });

      processed += 1;
      continue;
    }

    if (direction === "status") {
      const { ipAddress, userAgent } = await getRequestAuditContext();
      const messageId = event.status?.messageId;
      if (!messageId) {
        failed += 1;
        await writeWebhookEvent({
          tenantId: event.tenantId,
          channelId: resolvedChannelId,
          event,
          eventHash,
          processStatus: "failed",
          failedReason: "Status event missing messageId"
        });
        continue;
      }

      const targetMessage = await prisma.message.findFirst({
        where: {
          tenantId: event.tenantId,
          providerMessageId: messageId
        },
        include: { conversation: true }
      });

      const campaignRecipient = await prisma.campaignRecipient.findFirst({
        where: {
          tenantId: event.tenantId,
          providerMessageId: messageId
        }
      });

      if (!targetMessage && !campaignRecipient) {
        failed += 1;
        await writeWebhookEvent({
          tenantId: event.tenantId,
          channelId: resolvedChannelId,
          event,
          eventHash,
          processStatus: "failed",
          failedReason: "Message not found by providerMessageId"
        });
        await writeAuditTrail({
          tenantId: event.tenantId,
          actorUserId: null,
          entityType: "campaign_recipient",
          entityId: messageId,
          action: "campaign_delivery_sync_not_found",
          metadata: {
            providerMessageId: messageId,
            providerStatus: event.status?.deliveryStatus,
            eventType: event.eventType
          },
          ipAddress,
          userAgent
        }).catch(() => null);
        continue;
      }

      const mapped = getStatusDelivery(event.status?.deliveryStatus || "queued");
      const resolvedTargetStatus = mapped;
      const targetMessageStatus = targetMessage ? shouldAdvanceDeliveryStatus(targetMessage.deliveryStatus, resolvedTargetStatus) : resolvedTargetStatus;
      const campaignRecipientStatus = campaignRecipient
        ? shouldAdvanceDeliveryStatus(campaignRecipient.sendStatus, resolvedTargetStatus)
        : resolvedTargetStatus;

      const nowDelivery = campaignRecipientStatus === DeliveryStatus.DELIVERED || campaignRecipientStatus === DeliveryStatus.READ
        ? now
        : targetMessageStatus === DeliveryStatus.DELIVERED || targetMessageStatus === DeliveryStatus.READ
          ? now
          : undefined;
      const nowRead = campaignRecipientStatus === DeliveryStatus.READ || targetMessageStatus === DeliveryStatus.READ ? now : undefined;
      const txOps = [];
      const updateData = {
        deliveryStatus: targetMessageStatus,
        failedReason: campaignRecipientStatus === DeliveryStatus.FAILED ? event.status?.failureReason : null,
        deliveredAt: nowDelivery,
        readAt: nowRead
      };

      if (targetMessage) {
        txOps.push(
          prisma.message.update({
            where: { id: targetMessage.id },
            data: {
              ...updateData,
              sentAt: targetMessageStatus === DeliveryStatus.SENT && !targetMessage.sentAt ? now : undefined
            }
          })
        );
      }

      if (targetMessage) {
        txOps.push(
          prisma.conversationActivity.create({
            data: {
              tenantId: event.tenantId,
              conversationId: targetMessage.conversationId,
              actorUserId: null,
              activityType: "DELIVERY_UPDATE",
              metadataJson: JSON.stringify({
                providerStatus: mapped,
                providerEventId: event.providerEventId,
                messageId: targetMessage.id
              })
            }
          })
        );
      }

      if (campaignRecipient) {
        txOps.push(
          prisma.campaignRecipient.update({
            where: { id: campaignRecipient.id },
              data: {
              sendStatus: campaignRecipientStatus,
              failureReason: campaignRecipientStatus === DeliveryStatus.FAILED
                ? event.status?.failureReason ?? campaignRecipient?.failureReason ?? null
                : campaignRecipient?.failureReason ?? null,
              deliveredAt: nowDelivery,
              readAt: nowRead,
              sentAt: campaignRecipientStatus === DeliveryStatus.SENT && !campaignRecipient.sentAt ? now : campaignRecipient.sentAt,
              nextRetryAt: null
            }
          })
        );
      }

      txOps.push(
        writeWebhookEvent({
          tenantId: event.tenantId,
          channelId: resolvedChannelId,
          event,
          eventHash,
          processStatus: "processed"
        })
      );

      await Promise.all(txOps);

      if (campaignRecipient) {
        await recalculateCampaignTotals(campaignRecipient.campaignId);
      }

      await writeAuditTrail({
        tenantId: event.tenantId,
        actorUserId: null,
        entityType: campaignRecipient ? "campaign_recipient" : "message",
        entityId: campaignRecipient?.id || targetMessage?.id || messageId,
        action: "message_delivery_status_synced",
        metadata: {
          providerStatus: resolvedTargetStatus,
          appliedStatus: campaignRecipient ? campaignRecipientStatus : targetMessageStatus,
          providerMessageId: messageId
        },
        ipAddress,
        userAgent
      }).catch(() => null);

      processed += 1;
    }
  }

  return NextResponse.json({
    ok: true,
    processed,
    failed,
    skipped
  });
}
