import type { PromoCodeCampaignsRepository } from "@/internal/repository/promo-code-campaigns/promo-code-campaigns";
import { PromoCodesService } from "@/v1/services/admin/promo-codes/promo-codes.service";
import { logInfo } from "@/lib/logger";
import type {
  AttachPromoCodesRequest,
  CreateCampaignRequest,
  ListCampaignsQuery,
  UpdateCampaignRequest,
} from "./promo-code-campaigns.schema";

export class PromoCodeCampaignsService {
  constructor(private repo: PromoCodeCampaignsRepository) {}

  async create(body: CreateCampaignRequest, createdBy: string | null) {
    const existing = await this.repo.findByName(body.name);
    if (existing) {
      const err: any = new Error("A campaign with this name already exists");
      err.status = 409;
      throw err;
    }
    const created = await this.repo.create(body.name, createdBy);
    return created;
  }

  async update(
    id: string,
    body: UpdateCampaignRequest,
    updatedBy: string | null,
  ) {
    const current = await this.repo.getById(id);
    if (!current) {
      const err: any = new Error("Campaign not found");
      err.status = 404;
      throw err;
    }
    if (current.name !== body.name) {
      const dup = await this.repo.findByName(body.name);
      if (dup) {
        const err: any = new Error("A campaign with this name already exists");
        err.status = 409;
        throw err;
      }
    }
    return await this.repo.update(id, body.name, updatedBy);
  }

  async delete(id: string) {
    const current = await this.repo.getById(id);
    if (!current) {
      const err: any = new Error("Campaign not found");
      err.status = 404;
      throw err;
    }
    return await this.repo.delete(id);
  }

  async getById(id: string) {
    const c = await this.repo.getById(id);
    if (!c) {
      const err: any = new Error("Campaign not found");
      err.status = 404;
      throw err;
    }
    const promoCodes = await this.repo.listPromoCodes(id);
    return { ...c, promo_codes: promoCodes };
  }

  async list(query: ListCampaignsQuery, allowedIds?: string[] | null) {
    const page = Math.max(1, parseInt(query.page ?? "1", 10) || 1);
    const pageSize = Math.min(
      100,
      Math.max(1, parseInt(query.pageSize ?? "15", 10) || 15),
    );
    const offset = (page - 1) * pageSize;
    const { items, total } = await this.repo.list({
      limit: pageSize,
      offset,
      search: query.search,
      allowedIds,
    });
    return {
      items,
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.max(1, Math.ceil(total / pageSize)),
      },
    };
  }

  async listPromoCodes(campaignId: string) {
    const c = await this.repo.getById(campaignId);
    if (!c) {
      const err: any = new Error("Campaign not found");
      err.status = 404;
      throw err;
    }
    return await this.repo.listPromoCodes(campaignId);
  }

  async listAllPromoCodeMaps() {
    return await this.repo.listAllPromoCodeMaps();
  }

  async listCodeCampaignMap() {
    return await this.repo.listCodeCampaignMap();
  }

  // Union of live Updot promo codes and every promo code ever attached to a
  // campaign locally (including ones since deleted upstream). Powers the
  // Promocode Access List picker, which needs to offer historical codes for
  // assignment/reporting even after they no longer exist in Updot.
  async listAllKnownPromoCodes(): Promise<
    Array<{ id: string; name: string; isLive: boolean }>
  > {
    const promoSvc = PromoCodesService.getInstance();
    const PAGE = 500;
    const MAX_OFFSET = 5000;
    const byId = new Map<string, { id: string; name: string; isLive: boolean }>();

    let offset = 0;
    while (true) {
      const res = await promoSvc.listPromoCodes({
        includeExpired: "true",
        limit: String(PAGE),
        offset: String(offset),
      });
      const raw = (res as any)?.data;
      const items: any[] = Array.isArray(raw)
        ? raw
        : Array.isArray(raw?.data)
          ? raw.data
          : [];
      for (const it of items) {
        const name = String(it?.code ?? it?.name ?? "").trim();
        if (!name) continue;
        const id = String(it?.id ?? it?.promoCodeId ?? it?.promo_code_id ?? name);
        byId.set(id, { id, name, isLive: true });
      }
      if (items.length < PAGE) break;
      offset += PAGE;
      if (offset >= MAX_OFFSET) break;
    }

    const localCodes = await this.repo.listDistinctPromoCodeIds();
    for (const c of localCodes) {
      if (!byId.has(c.promo_code_id)) {
        byId.set(c.promo_code_id, {
          id: c.promo_code_id,
          name: c.promo_code_name,
          isLive: false,
        });
      }
    }

    return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
  }

  // Deleting a promo code in Updot doesn't notify this app, so
  // promo_code_campaign_map can accumulate rows pointing at codes that no
  // longer exist anywhere. Fetches the live Updot code set (including
  // expired) and detaches any local mapping whose promo_code_id isn't in it.
  async reconcileOrphanedPromoCodes(): Promise<{
    checked: number;
    orphaned: number;
    detached: Array<{ campaign_id: string; promo_code_id: string; promo_code_name: string }>;
  }> {
    const localCodes = await this.repo.listDistinctPromoCodeIds();
    if (localCodes.length === 0) {
      return { checked: 0, orphaned: 0, detached: [] };
    }

    const promoSvc = PromoCodesService.getInstance();
    const PAGE = 500;
    const MAX_OFFSET = 5000;
    const liveIds = new Set<string>();
    let offset = 0;
    while (true) {
      const res = await promoSvc.listPromoCodes({
        includeExpired: "true",
        limit: String(PAGE),
        offset: String(offset),
      });
      const raw = (res as any)?.data;
      const items: any[] = Array.isArray(raw)
        ? raw
        : Array.isArray(raw?.data)
          ? raw.data
          : [];
      for (const it of items) {
        liveIds.add(String(it?.id ?? it?.promoCodeId ?? it?.promo_code_id));
      }
      if (items.length < PAGE) break;
      offset += PAGE;
      if (offset >= MAX_OFFSET) break;
    }

    const orphaned = localCodes.filter((c) => !liveIds.has(c.promo_code_id));
    if (orphaned.length === 0) {
      return { checked: localCodes.length, orphaned: 0, detached: [] };
    }

    const detached = await this.repo.detachOrphanedPromoCodes(
      orphaned.map((c) => c.promo_code_id),
    );
    logInfo(
      `[PromoCodeCampaigns] Reconciliation detached ${detached.length} orphaned promo-code mapping(s): ${orphaned.map((c) => c.promo_code_name).join(", ")}`,
    );
    return { checked: localCodes.length, orphaned: orphaned.length, detached };
  }

  async attachPromoCodes(
    campaignId: string,
    body: AttachPromoCodesRequest,
    createdBy: string | null,
  ) {
    const c = await this.repo.getById(campaignId);
    if (!c) {
      const err: any = new Error("Campaign not found");
      err.status = 404;
      throw err;
    }
    await this.repo.attachPromoCodes(campaignId, body.promo_codes, createdBy);
    await this.repo.touchUpdated(campaignId, createdBy);
    return await this.repo.listPromoCodes(campaignId);
  }

  async detachPromoCode(
    campaignId: string,
    promoCodeId: string,
    updatedBy: string | null,
  ) {
    const c = await this.repo.getById(campaignId);
    if (!c) {
      const err: any = new Error("Campaign not found");
      err.status = 404;
      throw err;
    }
    const detached = await this.repo.detachPromoCode(campaignId, promoCodeId);
    if (!detached) {
      const err: any = new Error("Mapping not found");
      err.status = 404;
      throw err;
    }
    await this.repo.touchUpdated(campaignId, updatedBy);
    return detached;
  }

  async detachPromoCodes(
    campaignId: string,
    promoCodeIds: string[],
    updatedBy: string | null,
  ) {
    const c = await this.repo.getById(campaignId);
    if (!c) {
      const err: any = new Error("Campaign not found");
      err.status = 404;
      throw err;
    }
    const detached = await this.repo.detachPromoCodes(campaignId, promoCodeIds);
    await this.repo.touchUpdated(campaignId, updatedBy);
    return detached;
  }

  async dashboardOverview() {
    return await this.repo.listForDashboard();
  }
}
