import { Injectable, Logger } from '@nestjs/common';
import type { Affiliate, Campaign, Merchant } from '@prisma/client';
import { PrismaService } from '../common/prisma.service';
import { MarcadeoService } from './marcadeo.service';

/**
 * Bridges the real app actions to Marcadeo (the tracking backbone). Every method
 * is idempotent (stores the Marcadeo id back on the row and reuses it) and
 * fail-soft: if Marcadeo is unconfigured or errors, the app flow still succeeds —
 * we just log and move on, so the merchant/affiliate experience never breaks.
 *
 * Flow map:
 *   merchant (store)      → Marcadeo advertiser (User, role=advertiser)   [install]
 *   campaign (brand)      → Marcadeo campaign + Sale goal                 [createCampaign]
 *   affiliate (approved)  → Marcadeo publisher (User, role=affiliate)     [createPublisher]
 *                           + tracking link (real /click URL) + coupon
 */
@Injectable()
export class MarcadeoProvisioning {
  private readonly log = new Logger('Marcadeo');

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

  private on() {
    return this.marcadeo.configured();
  }

  /** Ensure this store exists as a Marcadeo advertiser; returns its id or null. */
  async ensureAdvertiser(merchant: Merchant): Promise<string | null> {
    if (!this.on()) return null;
    if (merchant.marcadeoAdvertiserId) return merchant.marcadeoAdvertiserId;
    try {
      const r = await this.marcadeo.install({
        shopDomain: merchant.shop,
        shopName: merchant.programName ?? merchant.shop,
        email: `${merchant.shop}@shopify.local`,
      });
      const advertiserId = String(r.advertiserId);
      await this.prisma.merchant.update({
        where: { id: merchant.id },
        data: { marcadeoAdvertiserId: advertiserId },
      });
      this.log.log(`advertiser ${advertiserId} for ${merchant.shop}`);
      return advertiserId;
    } catch (e) {
      this.log.warn(`ensureAdvertiser failed: ${(e as Error).message}`);
      return null;
    }
  }

  /**
   * S2S (server-to-server) info for a merchant: the postback URL template their
   * server can call to record a conversion, plus their IP whitelist. Marcadeo's
   * /track is the postback endpoint and already enforces the IP whitelist.
   */
  async s2sInfo(merchant: Merchant): Promise<{ postbackUrl: string; ips: Array<{ id: number; ip_address: string; type: string; is_active: boolean }> }> {
    const base = (process.env.MARCADEO_BASE_URL || '').replace(/\/$/, '');
    const postbackUrl = `${base}/track?click_id={click_id}&goal_id={goal_id}&sale_amount={sale_amount}&order_id={order_id}`;
    let ips: any[] = [];
    if (this.on() && merchant.marcadeoAdvertiserId) {
      try {
        ips = (await this.marcadeo.listIpWhitelist(merchant.marcadeoAdvertiserId)).ips;
      } catch (e) {
        this.log.warn(`s2sInfo list failed: ${(e as Error).message}`);
      }
    }
    return { postbackUrl, ips };
  }

  async s2sAddIp(merchant: Merchant, ip: string, type?: 'SINGLE' | 'CIDR'): Promise<boolean> {
    if (!this.on() || !merchant.marcadeoAdvertiserId) return false;
    try {
      await this.marcadeo.addIpWhitelist(merchant.marcadeoAdvertiserId, ip, type);
      return true;
    } catch (e) {
      this.log.warn(`s2sAddIp failed: ${(e as Error).message}`);
      return false;
    }
  }

  async s2sRemoveIp(merchant: Merchant, id: number | string): Promise<boolean> {
    if (!this.on() || !merchant.marcadeoAdvertiserId) return false;
    try {
      await this.marcadeo.removeIpWhitelist(merchant.marcadeoAdvertiserId, id);
      return true;
    } catch (e) {
      this.log.warn(`s2sRemoveIp failed: ${(e as Error).message}`);
      return false;
    }
  }

  /** Push the merchant's program name to their Marcadeo advertiser. Fail-soft. */
  async syncAdvertiserProfile(merchant: Merchant): Promise<void> {
    if (!this.on() || !merchant.marcadeoAdvertiserId) return;
    try {
      await this.marcadeo.updateAdvertiser({
        advertiserId: merchant.marcadeoAdvertiserId,
        name: merchant.programName ?? merchant.shop,
      });
    } catch (e) {
      this.log.warn(`syncAdvertiserProfile failed: ${(e as Error).message}`);
    }
  }

  /** Ensure a campaign exists in Marcadeo; stores marcadeoCampaignId + goalId. */
  async ensureCampaign(merchant: Merchant, campaign: Campaign): Promise<{ campaignId: string; goalId?: string } | null> {
    if (!this.on()) return null;
    if (campaign.marcadeoCampaignId) {
      return { campaignId: campaign.marcadeoCampaignId, goalId: campaign.marcadeoGoalId ?? undefined };
    }
    const advertiserId = await this.ensureAdvertiser(merchant);
    if (!advertiserId) return null;
    try {
      const r = await this.marcadeo.createCampaign({
        advertiserId,
        name: campaign.name,
        payoutType: campaign.commissionType === 'FLAT' ? 'flat' : 'percent',
        payoutValue: Number(campaign.commissionValue) || 0,
      });
      const campaignId = String(r.campaignId);
      const goalId = r.goalId != null ? String(r.goalId) : undefined;
      await this.prisma.campaign.update({
        where: { id: campaign.id },
        data: { marcadeoCampaignId: campaignId, marcadeoGoalId: goalId },
      });
      this.log.log(`campaign ${campaignId} (${campaign.name})`);
      return { campaignId, goalId };
    } catch (e) {
      this.log.warn(`ensureCampaign failed: ${(e as Error).message}`);
      return null;
    }
  }

  /**
   * Sync a campaign's extra goals (conversion events) to Marcadeo. The primary
   * "Sale" goal is created with the campaign itself; this handles any additional
   * goals (Lead / Signup / …) the merchant defined. Idempotent — only creates
   * goals that don't yet have a marcadeoGoalId. Fail-soft.
   */
  async ensureCampaignGoals(campaign: Campaign): Promise<void> {
    if (!this.on() || !campaign.marcadeoCampaignId) return;
    const goals = await this.prisma.campaignGoal.findMany({
      where: { campaignId: campaign.id, marcadeoGoalId: null },
    });
    for (const g of goals) {
      try {
        const r = await this.marcadeo.createGoal({
          campaignId: campaign.marcadeoCampaignId,
          name: g.name,
          goalModel: g.model as 'CPS' | 'CPA' | 'CPL' | 'CPI' | 'CPM',
          payoutType: g.commissionType === 'FLAT' ? 'flat' : 'percent',
          payoutValue: Number(g.commissionValue) || 0,
          isPrimary: g.isPrimary,
        });
        await this.prisma.campaignGoal.update({
          where: { id: g.id },
          data: { marcadeoGoalId: String(r.goalId) },
        });
      } catch (e) {
        this.log.warn(`ensureCampaignGoals (${g.name}) failed: ${(e as Error).message}`);
      }
    }
  }

  /**
   * Ensure an approved affiliate exists as a Marcadeo publisher, and build a real
   * tracking link + attach their coupon. Stores marcadeoPublisherId +
   * marcadeoTrackingUrl on the affiliate. Uses the merchant's default campaign.
   */
  async ensurePublisher(merchant: Merchant, affiliate: Affiliate): Promise<string | null> {
    if (!this.on()) return null;

    let publisherId = affiliate.marcadeoPublisherId;
    if (!publisherId) {
      try {
        const r = await this.marcadeo.createPublisher({
          name: affiliate.name,
          email: affiliate.email,
          externalRef: affiliate.id,
        });
        publisherId = String(r.publisherId);
        await this.prisma.affiliate.update({
          where: { id: affiliate.id },
          data: { marcadeoPublisherId: publisherId },
        });
        this.log.log(`publisher ${publisherId} (${affiliate.email})`);
      } catch (e) {
        this.log.warn(`ensurePublisher failed: ${(e as Error).message}`);
        return null;
      }
    }

    // A campaign to hang the link/coupon on. Prefer an already-synced campaign.
    const campaign = await this.prisma.campaign.findFirst({
      where: { merchantId: merchant.id, marcadeoCampaignId: { not: null } },
      orderBy: { createdAt: 'asc' },
    });
    const marcadeoCampaignId = campaign?.marcadeoCampaignId;
    if (!marcadeoCampaignId) return publisherId;

    // Real tracking link (Marcadeo /click URL) + coupon mapping.
    try {
      const link = await this.marcadeo.createTrackingLink({
        campaignId: marcadeoCampaignId,
        publisherId,
        subId: affiliate.id,
      });
      if (link.url) {
        await this.prisma.affiliate.update({
          where: { id: affiliate.id },
          data: { marcadeoTrackingUrl: link.url },
        });
      }
    } catch (e) {
      this.log.warn(`createTrackingLink failed: ${(e as Error).message}`);
    }

    if (affiliate.couponCode) {
      try {
        await this.marcadeo.attachCoupon({
          campaignId: marcadeoCampaignId,
          userId: publisherId,
          couponCode: affiliate.couponCode,
        });
      } catch (e) {
        this.log.warn(`attachCoupon failed: ${(e as Error).message}`);
      }
    }

    // Push this affiliate's individual rate (if any) into Marcadeo.
    await this.syncAffiliatePayout(merchant, { ...affiliate, marcadeoPublisherId: publisherId });

    return publisherId;
  }

  /**
   * Push an affiliate's per-affiliate commission override into Marcadeo as a
   * per-publisher payout (UserPayout) on every synced campaign — so the tracking
   * backbone pays this affiliate their individual rate, not the campaign default.
   * No-op when the affiliate has no override. Fail-soft.
   */
  async syncAffiliatePayout(merchant: Merchant, affiliate: Affiliate): Promise<void> {
    if (!this.on()) return;
    if (!affiliate.commissionType || affiliate.commissionValue == null) return; // no override
    const publisherId = affiliate.marcadeoPublisherId;
    if (!publisherId) return;

    const campaigns = await this.prisma.campaign.findMany({
      where: { merchantId: merchant.id, marcadeoCampaignId: { not: null } },
    });
    for (const c of campaigns) {
      try {
        await this.marcadeo.setPublisherPayout({
          campaignId: c.marcadeoCampaignId!,
          userId: publisherId,
          payoutType: affiliate.commissionType === 'FLAT' ? 'flat' : 'percent',
          payoutValue: Number(affiliate.commissionValue) || 0,
        });
      } catch (e) {
        this.log.warn(`syncAffiliatePayout (${c.name}) failed: ${(e as Error).message}`);
      }
    }
  }

  /**
   * A tracked DEEP link for one affiliate to a specific product/collection URL.
   * Creates (or reuses) a Marcadeo link whose destination is that URL, and
   * returns the affiliate's /click URL for it. Uses the first synced campaign.
   */
  async affiliateDeepLink(
    merchant: Merchant,
    affiliate: Affiliate,
    destination: string,
  ): Promise<string | null> {
    if (!this.on()) return null;
    const publisherId = affiliate.marcadeoPublisherId ?? (await this.ensurePublisher(merchant, affiliate));
    if (!publisherId) return null;
    const campaign = await this.prisma.campaign.findFirst({
      where: { merchantId: merchant.id, active: true, marcadeoCampaignId: { not: null } },
      orderBy: { createdAt: 'asc' },
    });
    if (!campaign?.marcadeoCampaignId) return null;
    try {
      const link = await this.marcadeo.createTrackingLink({
        campaignId: campaign.marcadeoCampaignId,
        publisherId,
        subId: affiliate.id,
        destination,
      });
      return link.url || null;
    } catch (e) {
      this.log.warn(`affiliateDeepLink failed: ${(e as Error).message}`);
      return null;
    }
  }

  /**
   * Every brand/campaign link this affiliate can promote. One publisher (uid),
   * one /click link per campaign (differing oid + lid). Generated on demand so a
   * campaign created AFTER approval automatically shows up. The affiliate's
   * coupon is shared across brands (orders are split by product), so it's
   * returned alongside each link for convenience.
   *
   * Returns [] when Marcadeo isn't configured — the caller falls back to the
   * single stored referral link.
   */
  async affiliateCampaignLinks(
    merchant: Merchant,
    affiliate: Affiliate,
  ): Promise<Array<{ campaignId: string; name: string; url: string; couponCode: string | null }>> {
    if (!this.on()) return [];
    const publisherId = affiliate.marcadeoPublisherId ?? (await this.ensurePublisher(merchant, affiliate));
    if (!publisherId) return [];

    const campaigns = await this.prisma.campaign.findMany({
      where: { merchantId: merchant.id, active: true, marcadeoCampaignId: { not: null } },
      orderBy: { createdAt: 'asc' },
    });

    const links: Array<{ campaignId: string; name: string; url: string; couponCode: string | null }> = [];
    for (const c of campaigns) {
      try {
        const link = await this.marcadeo.createTrackingLink({
          campaignId: c.marcadeoCampaignId!,
          publisherId,
          subId: affiliate.id,
        });
        if (link.url) {
          links.push({ campaignId: c.id, name: c.name, url: link.url, couponCode: affiliate.couponCode });
        }
      } catch (e) {
        this.log.warn(`link for campaign ${c.name} failed: ${(e as Error).message}`);
      }
    }
    return links;
  }
}
