import { Injectable, Logger } from '@nestjs/common';
import type { Merchant } from '@prisma/client';
import { PrismaService } from '../common/prisma.service';
import { MarcadeoService } from '../integrations/marcadeo.service';
import { ShopifyService } from '../integrations/shopify.service';
import { calcCommission } from '../common/core';

/** A store-agnostic order shape both the webhook and manual sync normalize to. */
export interface NormalizedOrder {
  id: string;
  email: string | null;
  subtotal: number;
  total: number;
  discountCodes: string[];
  lineItems: { title: string; vendor: string; amount: number }[];
  tags: string[];
  clickId?: string | null; // Marcadeo click_id captured at the storefront
  isNewCustomer?: boolean; // undefined = unknown (don't enforce new-customer rule)
}

/**
 * Single source of truth for turning a Shopify order into affiliate credit.
 * Used by BOTH the real-time orders/create webhook and the manual sync-orders
 * button, so attribution rules never drift between the two.
 *
 * Two systems are updated:
 *  - App DB (ReferralOrder + affiliate balance)  → coupon-based attribution
 *  - Marcadeo /track (the tracking backbone)      → when a click_id is present
 */
@Injectable()
export class OrderAttributionService {
  private readonly log = new Logger('Attribution');

  constructor(
    private prisma: PrismaService,
    private marcadeo: MarcadeoService,
    private shopify: ShopifyService,
  ) {}

  async attribute(
    merchant: Merchant,
    o: NormalizedOrder,
  ): Promise<{ status: 'attributed' | 'skipped'; reason?: string }> {
    const codes = o.discountCodes.map((c) => c.toUpperCase()).filter(Boolean);
    const emailKey = o.email ? o.email.toLowerCase() : null;

    // Idempotent — never double-credit the same Shopify order.
    const exists = await this.prisma.referralOrder.findFirst({
      where: { merchantId: merchant.id, shopifyOrderId: o.id },
    });
    if (exists) return { status: 'skipped', reason: 'already recorded' };

    // Resolve the affiliate: by coupon first, else by a customer→affiliate binding.
    let affiliate = codes.length
      ? await this.prisma.affiliate.findFirst({
          where: { merchantId: merchant.id, status: 'ACTIVE', couponCode: { in: codes, mode: 'insensitive' } },
          include: { group: true },
        })
      : null;
    if (!affiliate && merchant.customerAffiliateConnect && emailKey) {
      const bind = await this.prisma.customerAffiliate.findUnique({
        where: { merchantId_customerEmail: { merchantId: merchant.id, customerEmail: emailKey } },
      });
      if (bind) {
        affiliate = await this.prisma.affiliate.findFirst({
          where: { id: bind.affiliateId, status: 'ACTIVE' },
          include: { group: true },
        });
      }
    }
    if (!affiliate) {
      return { status: 'skipped', reason: codes.length ? 'no active affiliate' : 'no coupon / no bound customer' };
    }

    // Global rule: don't record a sale the affiliate made to themselves.
    if (
      merchant.excludeSelfPurchase &&
      o.email &&
      affiliate.email &&
      o.email.toLowerCase() === affiliate.email.toLowerCase()
    ) {
      return { status: 'skipped', reason: 'self-purchase (excluded)' };
    }
    // Global rule: pay only for new customers → zero out commission for repeats.
    const newCustomerZero = merchant.commissionNewCustomersOnly && o.isNewCustomer === false;

    const orderTags = o.tags.map((t) => t.toLowerCase());
    const campaigns = await this.prisma.campaign.findMany({ where: { merchantId: merchant.id, active: true } });

    // --- Multi-campaign split: resolve each line-item to a brand/campaign. ---
    const byCampaign = new Map<string, number>();
    if (campaigns.length && !affiliate.couponPersonal) {
      for (const it of o.lineItems) {
        const c = campaigns.find((c) => {
          const v = c.matchValue.toLowerCase();
          if (c.matchType === 'ALL') return true;
          if (c.matchType === 'VENDOR') return (it.vendor || '').toLowerCase() === v;
          if (c.matchType === 'PRODUCT') return (it.title || '').toLowerCase().includes(v);
          if (c.matchType === 'TAG') return orderTags.includes(v);
          return false;
        });
        if (c) byCampaign.set(c.id, (byCampaign.get(c.id) || 0) + Number(it.amount));
      }
    }

    // Campaigns we post to Marcadeo /track for (goal + the split amount).
    const marcadeoPosts: { goalId: string; amount: number }[] = [];

    let recorded = 0;
    let directCommission = 0;
    if (byCampaign.size > 0) {
      for (const [campaignId, sub] of byCampaign) {
        const c = campaigns.find((x) => x.id === campaignId)!;
        const commission = newCustomerZero
          ? 0
          : c.commissionType === 'FLAT'
            ? Number(c.commissionValue)
            : (sub * Number(c.commissionValue)) / 100;
        // Skip ₹0-commission sales when the merchant opts out of recording them.
        if (commission === 0 && !merchant.recordNilSales) continue;
        recorded++;
        directCommission += commission;
        await this.prisma.referralOrder.create({
          data: {
            merchantId: merchant.id,
            affiliateId: affiliate.id,
            campaignId: c.id,
            shopifyOrderId: o.id,
            attribution: o.clickId ? 'click' : 'coupon',
            customerEmail: o.email,
            subtotal: sub,
            total: sub,
            commission,
            status: 'APPROVED',
          },
        });
        await this.prisma.affiliate.update({ where: { id: affiliate.id }, data: { balance: { increment: commission } } });
        if (c.marcadeoGoalId) marcadeoPosts.push({ goalId: c.marcadeoGoalId, amount: sub });
      }
    } else {
      // --- Fallback: single attribution (no per-brand match). ---
      const now = new Date();
      const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
      const salesThisMonth = await this.prisma.referralOrder.count({
        where: { affiliateId: affiliate.id, status: 'APPROVED', createdAt: { gte: monthStart } },
      });
      let { commission } = calcCommission({ merchant, affiliate, subtotal: o.subtotal, total: o.total, salesThisMonth });

      if (affiliate.couponPersonal) {
        commission = 0;
      } else {
        const rules = await this.prisma.productCommission.findMany({
          where: { merchantId: merchant.id, OR: [{ affiliateId: affiliate.id }, { affiliateId: null }] },
          orderBy: { affiliateId: 'desc' },
        });
        const titles = o.lineItems.map((l) => l.title.toLowerCase());
        const base = merchant.commissionBase === 'TOTAL' ? o.total : o.subtotal;
        for (const r of rules) {
          const v = r.matchValue.toLowerCase();
          const hit = r.matchType === 'TAG' ? orderTags.includes(v) : titles.some((t) => t.includes(v));
          if (hit) {
            commission = r.commissionType === 'FLAT' ? Number(r.commissionValue) : (base * Number(r.commissionValue)) / 100;
            break;
          }
        }
      }
      if (newCustomerZero) commission = 0;
      // Skip ₹0-commission sales when the merchant opts out of recording them.
      if (commission === 0 && !merchant.recordNilSales) {
        return { status: 'skipped', reason: 'nil sale (not recorded)' };
      }
      recorded++;
      directCommission += commission;
      await this.prisma.referralOrder.create({
        data: {
          merchantId: merchant.id,
          affiliateId: affiliate.id,
          shopifyOrderId: o.id,
          attribution: o.clickId ? 'click' : 'coupon',
          customerEmail: o.email,
          subtotal: o.subtotal,
          total: o.total,
          commission,
          status: 'APPROVED',
        },
      });
      await this.prisma.affiliate.update({ where: { id: affiliate.id }, data: { balance: { increment: commission } } });

      // No brand matched — post against the first synced campaign's goal, if any.
      const firstGoal = campaigns.find((c) => c.marcadeoGoalId)?.marcadeoGoalId;
      if (firstGoal) marcadeoPosts.push({ goalId: firstGoal, amount: o.total });
    }

    // Everything was a not-recorded nil sale.
    if (recorded === 0) return { status: 'skipped', reason: 'nil sale (not recorded)' };

    // --- Marcadeo conversion (needs the click that generated this sale). ---
    if (o.clickId && this.marcadeo.configured()) {
      for (const p of marcadeoPosts) {
        try {
          await this.marcadeo.postConversion({
            clickId: o.clickId,
            goalId: p.goalId,
            orderId: o.id,
            amount: p.amount,
            status: 'approved',
          });
        } catch (e) {
          this.log.warn(`Marcadeo /track failed for order ${o.id}: ${(e as Error).message}`);
        }
      }
    }

    // Audit trail: tag the order in Shopify so the merchant sees the attribution.
    if (merchant.shopifyAccessToken) {
      const label = affiliate.couponCode || affiliate.name;
      await this.shopify.tagOrder(o.id, ['trackopia', `affiliate:${label}`], merchant.shop, merchant.shopifyAccessToken);
    }

    // Bind this customer to the affiliate so future orders auto-attribute.
    if (merchant.customerAffiliateConnect && emailKey) {
      await this.prisma.customerAffiliate.upsert({
        where: { merchantId_customerEmail: { merchantId: merchant.id, customerEmail: emailKey } },
        create: { merchantId: merchant.id, customerEmail: emailKey, affiliateId: affiliate.id },
        update: {}, // keep the first affiliate that referred them
      });
    }

    // Multi-level: pay override commission up the referral chain.
    await this.payMlmOverrides(merchant, affiliate, directCommission, o.id);

    return { status: 'attributed' };
  }

  /**
   * Pay override commission to the affiliate's uplines. mlmLevels = [10,5,2] means
   * the direct recruiter gets 10% of the affiliate's commission, their recruiter
   * 5%, and so on. Recorded as separate 'mlm' ReferralOrders (id `<order>::mlmN`).
   */
  private async payMlmOverrides(
    merchant: Merchant,
    affiliate: { referredById: string | null },
    baseCommission: number,
    orderId: string,
  ): Promise<void> {
    if (!merchant.mlmEnabled || baseCommission <= 0) return;
    const levels = Array.isArray(merchant.mlmLevels) ? (merchant.mlmLevels as unknown[]) : [];
    if (!levels.length) return;

    let currentId = affiliate.referredById;
    for (let lvl = 0; lvl < levels.length && currentId; lvl++) {
      const parent = await this.prisma.affiliate.findUnique({ where: { id: currentId } });
      if (!parent || parent.status !== 'ACTIVE') break;
      const pct = Number(levels[lvl]) || 0;
      const override = Math.round(((baseCommission * pct) / 100) * 100) / 100;
      if (override > 0) {
        await this.prisma.referralOrder.create({
          data: {
            merchantId: merchant.id,
            affiliateId: parent.id,
            shopifyOrderId: `${orderId}::mlm${lvl + 1}`,
            attribution: 'mlm',
            subtotal: 0,
            total: 0,
            commission: override,
            status: 'APPROVED',
          },
        });
        await this.prisma.affiliate.update({ where: { id: parent.id }, data: { balance: { increment: override } } });
      }
      currentId = parent.referredById;
    }
  }

  /**
   * Reverse a refunded order: mark its attributions REFUNDED, claw back the
   * commission from the affiliate's balance, and reverse the Marcadeo conversion.
   * Idempotent — already-refunded orders are skipped.
   */
  async reverse(merchant: Merchant, shopifyOrderId: string): Promise<{ reversed: number }> {
    const orders = await this.prisma.referralOrder.findMany({
      where: {
        merchantId: merchant.id,
        status: { not: 'REFUNDED' },
        // The direct sale + any MLM override rows (`<order>::mlmN`).
        OR: [{ shopifyOrderId }, { shopifyOrderId: { startsWith: `${shopifyOrderId}::mlm` } }],
      },
    });
    let reversed = 0;
    for (const o of orders) {
      await this.prisma.referralOrder.update({
        where: { id: o.id },
        data: { status: 'REFUNDED', refundedAt: new Date() },
      });
      const commission = Number(o.commission) || 0;
      if (commission > 0) {
        await this.prisma.affiliate.update({
          where: { id: o.affiliateId },
          data: { balance: { decrement: commission } },
        });
      }
      reversed++;
    }
    if (reversed && this.marcadeo.configured()) {
      try {
        await this.marcadeo.reverseConversion({ orderId: shopifyOrderId });
      } catch (e) {
        this.log.warn(`Marcadeo reverse failed for order ${shopifyOrderId}: ${(e as Error).message}`);
      }
    }
    return { reversed };
  }
}
