import { Controller, Post, Req, Res, Headers, Logger } from '@nestjs/common';
import type { Request, Response } from 'express';
import { PrismaService } from '../common/prisma.service';
import { ShopifyAuthService } from './shopify-auth.service';
import { OrderAttributionService, NormalizedOrder } from '../orders/order-attribution.service';
import { logDataAccess, maskEmail } from '../common/core';

/** click_id can arrive under any of these order note-attribute names. */
const CLICK_ID_KEYS = ['click_id', 'marcadeo_click_id', 'clickid'];

/**
 * Shopify webhook receivers. Every request is HMAC-verified against the raw body
 * (captured in main.ts) before we trust it. Handlers must respond 2xx fast or
 * Shopify retries and eventually disables the webhook.
 */
@Controller('webhooks')
export class WebhooksController {
  private readonly log = new Logger('Webhooks');

  constructor(
    private prisma: PrismaService,
    private auth: ShopifyAuthService,
    private attribution: OrderAttributionService,
  ) {}

  private verify(req: Request, hmac?: string): boolean {
    const raw = (req as any).rawBody as Buffer | undefined;
    if (!raw) return false;
    return this.auth.verifyWebhookHmac(raw, hmac);
  }

  /** app/uninstalled — merchant removed the app: revoke token, mark uninstalled. */
  @Post('app-uninstalled')
  async appUninstalled(
    @Req() req: Request,
    @Res() res: Response,
    @Headers('x-shopify-hmac-sha256') hmac: string,
    @Headers('x-shopify-shop-domain') shop: string,
  ) {
    if (!this.verify(req, hmac)) return res.status(401).send('bad hmac');
    if (this.auth.validShop(shop)) {
      await this.prisma.merchant.updateMany({
        where: { shop },
        data: { appInstalled: false, shopifyAccessToken: null, uninstalledAt: new Date() },
      });
      this.log.log(`uninstalled ${shop}`);
    }
    return res.status(200).send('ok');
  }

  /**
   * orders/create — real-time conversion. HMAC-verify, resolve the merchant by
   * shop domain, normalize the order, then run the shared attribution (app DB +
   * Marcadeo /track when a click_id is present). Always 200 after logging so
   * Shopify keeps the subscription healthy — the manual sync-orders is a backstop
   * and attribution is idempotent, so a missed/retried webhook is harmless.
   */
  @Post('orders-create')
  async ordersCreate(
    @Req() req: Request,
    @Res() res: Response,
    @Headers('x-shopify-hmac-sha256') hmac: string,
    @Headers('x-shopify-shop-domain') shop: string,
  ) {
    if (!this.verify(req, hmac)) return res.status(401).send('bad hmac');
    try {
      const merchant = this.auth.validShop(shop)
        ? await this.prisma.merchant.findUnique({ where: { shop } })
        : null;
      if (!merchant) {
        this.log.warn(`orders/create for unknown shop ${shop}`);
        return res.status(200).send('ok');
      }
      const order = this.normalize(req.body);
      // Audit: this order carried customer personal data (email) that we processed.
      if (order.email) await logDataAccess(this.prisma, merchant.id, 'order.customer_email', `${maskEmail(order.email)} order:${order.id}`);
      const r = await this.attribution.attribute(merchant, order);
      this.log.log(`orders/create ${shop} order ${order.id}: ${r.status}${r.reason ? ` (${r.reason})` : ''}`);
    } catch (e) {
      this.log.error(`orders/create failed for ${shop}: ${(e as Error).message}`);
    }
    return res.status(200).send('ok');
  }

  /**
   * orders/updated — auto-verify sales. When "Verify Sales Automatically" is on
   * and the order is now cancelled / voided / fully refunded, reverse its
   * attribution (claw back commission). Other status changes are left as-is.
   */
  @Post('orders-updated')
  async ordersUpdated(
    @Req() req: Request,
    @Res() res: Response,
    @Headers('x-shopify-hmac-sha256') hmac: string,
    @Headers('x-shopify-shop-domain') shop: string,
  ) {
    if (!this.verify(req, hmac)) return res.status(401).send('bad hmac');
    try {
      const merchant = this.auth.validShop(shop)
        ? await this.prisma.merchant.findUnique({ where: { shop } })
        : null;
      const orderId = req.body?.id != null ? String(req.body.id) : '';
      if (merchant?.verifySalesAutomatically && orderId) {
        const cancelled = !!req.body?.cancelled_at;
        const fin = String(req.body?.financial_status || '').toLowerCase();
        if (cancelled || fin === 'voided' || fin === 'refunded') {
          const r = await this.attribution.reverse(merchant, orderId);
          this.log.log(`orders/updated ${shop} order ${orderId}: auto-reversed ${r.reversed} (${cancelled ? 'cancelled' : fin})`);
        }
      }
    } catch (e) {
      this.log.error(`orders/updated failed for ${shop}: ${(e as Error).message}`);
    }
    return res.status(200).send('ok');
  }

  /**
   * refunds/create — an order was refunded. Reverse the attribution: mark the
   * ReferralOrder(s) REFUNDED, claw back commission, reverse the Marcadeo
   * conversion. HMAC-verified; always 200 (idempotent).
   */
  @Post('refunds-create')
  async refundsCreate(
    @Req() req: Request,
    @Res() res: Response,
    @Headers('x-shopify-hmac-sha256') hmac: string,
    @Headers('x-shopify-shop-domain') shop: string,
  ) {
    if (!this.verify(req, hmac)) return res.status(401).send('bad hmac');
    try {
      const merchant = this.auth.validShop(shop)
        ? await this.prisma.merchant.findUnique({ where: { shop } })
        : null;
      const orderId = req.body?.order_id != null ? String(req.body.order_id) : '';
      if (merchant && orderId) {
        const r = await this.attribution.reverse(merchant, orderId);
        this.log.log(`refunds/create ${shop} order ${orderId}: reversed ${r.reversed}`);
      }
    } catch (e) {
      this.log.error(`refunds/create failed for ${shop}: ${(e as Error).message}`);
    }
    return res.status(200).send('ok');
  }

  // ---- GDPR mandatory compliance webhooks (required for App Store) ----

  /** customers/data_request — merchant asked for a customer's data. Acknowledge. */
  @Post('customers-data-request')
  async customersDataRequest(@Req() req: Request, @Res() res: Response, @Headers('x-shopify-hmac-sha256') hmac: string) {
    if (!this.verify(req, hmac)) return res.status(401).send('bad hmac');
    // We store no customer PII beyond an optional order email; nothing to compile.
    this.log.log(`customers/data_request for ${req.body?.shop_domain}`);
    const m = await this.prisma.merchant.findUnique({ where: { shop: req.body?.shop_domain ?? '' } }).catch(() => null);
    await logDataAccess(this.prisma, m?.id ?? null, 'gdpr.data_request', maskEmail(req.body?.customer?.email));
    return res.status(200).send('ok');
  }

  /** customers/redact — erase a specific customer's data. */
  @Post('customers-redact')
  async customersRedact(@Req() req: Request, @Res() res: Response, @Headers('x-shopify-hmac-sha256') hmac: string) {
    if (!this.verify(req, hmac)) return res.status(401).send('bad hmac');
    try {
      const shop = req.body?.shop_domain as string;
      const email = req.body?.customer?.email as string | undefined;
      if (shop && email) {
        const merchant = await this.prisma.merchant.findUnique({ where: { shop } });
        if (merchant) {
          await this.prisma.referralOrder.updateMany({
            where: { merchantId: merchant.id, customerEmail: email },
            data: { customerEmail: null },
          });
          await logDataAccess(this.prisma, merchant.id, 'gdpr.customer_redact', maskEmail(email));
        }
      }
    } catch (e) {
      this.log.error(`customers/redact failed: ${(e as Error).message}`);
    }
    return res.status(200).send('ok');
  }

  /** shop/redact — erase all of a shop's data (48h after uninstall). */
  @Post('shop-redact')
  async shopRedact(@Req() req: Request, @Res() res: Response, @Headers('x-shopify-hmac-sha256') hmac: string) {
    if (!this.verify(req, hmac)) return res.status(401).send('bad hmac');
    try {
      const shop = req.body?.shop_domain as string;
      if (this.auth.validShop(shop)) {
        // Cascade deletes affiliates, orders, payouts, campaigns, etc.
        await this.prisma.merchant.deleteMany({ where: { shop } });
        this.log.log(`shop/redact — erased ${shop}`);
        // merchantId is intentionally non-relational so this survives the delete.
        await logDataAccess(this.prisma, null, 'gdpr.shop_redact', shop);
      }
    } catch (e) {
      this.log.error(`shop/redact failed: ${(e as Error).message}`);
    }
    return res.status(200).send('ok');
  }

  /** Shopify REST orders/create payload -> the attribution service's shape. */
  private normalize(body: any): NormalizedOrder {
    const noteAttrs: { name: string; value: string }[] = body?.note_attributes ?? [];
    const clickAttr = noteAttrs.find((a) => CLICK_ID_KEYS.includes(String(a.name).toLowerCase()));
    const tags =
      typeof body?.tags === 'string'
        ? body.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
        : Array.isArray(body?.tags)
          ? body.tags
          : [];
    return {
      id: String(body?.id ?? ''),
      email: body?.email ?? null,
      subtotal: Number(body?.subtotal_price) || 0,
      total: Number(body?.total_price) || 0,
      discountCodes: (body?.discount_codes ?? []).map((d: any) => String(d?.code ?? '')).filter(Boolean),
      lineItems: (body?.line_items ?? []).map((li: any) => ({
        title: String(li?.title ?? ''),
        vendor: String(li?.vendor ?? ''),
        amount: Number(li?.price ?? 0) * Number(li?.quantity ?? 1),
      })),
      tags,
      clickId: clickAttr?.value || null,
      // orders_count includes THIS order, so 1 (or less) means a new customer.
      isNewCustomer:
        body?.customer?.orders_count != null ? Number(body.customer.orders_count) <= 1 : undefined,
    };
  }
}
