/** Export sections that can be controlled per access-list member. */
export const EXPORT_SECTIONS = [
  "campaigns",
  "promo_codes",
  "member_referrals",
  "reports",
  "bookings",
  // Member personal data, split out from `campaigns` — see the
  // 20260806000000 migration.
  "promo_code_members",
] as const;
export type ExportSectionKey = (typeof EXPORT_SECTIONS)[number];

/** download = direct, request = needs approval, none = no export at all. */
export const EXPORT_MODES = ["download", "request", "none"] as const;

/**
 * Sections that also have a legacy `can_export_<section>` boolean column.
 *
 * `bookings` is absent on purpose: it was added with the modes and never had a
 * boolean. Mirroring every mode to a boolean therefore tried to write
 * `can_export_bookings`, which does not exist, and the whole save failed with
 * 42703. The booleans exist only for backward compatibility, so a section added
 * from now on simply doesn't get one.
 */
const SECTIONS_WITH_LEGACY_BOOLEAN = new Set([
  "campaigns",
  "promo_codes",
  "member_referrals",
  "reports",
]);

/**
 * The boolean column each section falls back to when its mode is unset.
 *
 * `bookings` and `promo_code_members` have no boolean of their own, and reading a
 * column that doesn't exist yields `undefined` — which is not `false`, so the
 * fallback below would resolve them to `download`. For `promo_code_members` that
 * would mean handing out member personal data to someone whose Campaigns setting
 * says otherwise, since Campaigns is what governed this export before it was split
 * out. Both therefore fall back to the Campaigns boolean, which is exactly what
 * gated them at the time any such row was written.
 */
const LEGACY_BOOLEAN_FOR: Record<string, string> = {
  campaigns: "can_export_campaigns",
  promo_codes: "can_export_promo_codes",
  member_referrals: "can_export_member_referrals",
  reports: "can_export_reports",
  bookings: "can_export_campaigns",
  promo_code_members: "can_export_campaigns",
};
export type ExportMode = (typeof EXPORT_MODES)[number];

import { logError } from "@/lib/logger";
import type { TConsoleUserRepository } from "@/internal/console-users/console-users.repository";
import type { TConsoleUserAccessListRepository } from "./console-user-access-list.repository";

type TConsoleUserAccessListServiceDeps = {
  ConsoleUserAccessListRepository: TConsoleUserAccessListRepository;
  ConsoleUserRepository: TConsoleUserRepository;
};

export const ConsoleUserAccessListServices = ({
  ConsoleUserAccessListRepository,
  ConsoleUserRepository,
}: TConsoleUserAccessListServiceDeps) => {
  const GetUserAccess = async (userId: string) => {
    try {
      const [promoCodeIds, campaignIds] = await Promise.all([
        ConsoleUserAccessListRepository.FindPromoCodeIdsByUserId(userId),
        ConsoleUserAccessListRepository.FindCampaignIdsByUserId(userId),
      ]);
      return { promoCodeIds, campaignIds };
    } catch (err) {
      logError(err);
      throw new Error(
        (err as Error).message ?? "Failed fetching user access list",
      );
    }
  };

  const SetUserAccess = async (
    userId: string,
    entry: {
      promoCodeIds: string[];
      campaignIds: string[];
      canExportCampaigns?: boolean;
      canExportPromoCodes?: boolean;
      canExportMemberReferrals?: boolean;
      canExportReports?: boolean;
      /**
       * Per-section export mode: download | request | none.
       *
       * Supersedes the booleans above. Both are written while the old columns
       * exist so the two can never disagree; the mode is what the console reads.
       */
      exportModes?: Record<string, string>;
    },
  ) => {
    try {
      const exportFlags: Record<string, boolean | string> = {};
      if (entry.canExportCampaigns !== undefined) {
        exportFlags.can_export_campaigns = entry.canExportCampaigns;
      }
      if (entry.canExportPromoCodes !== undefined) {
        exportFlags.can_export_promo_codes = entry.canExportPromoCodes;
      }
      if (entry.canExportMemberReferrals !== undefined) {
        exportFlags.can_export_member_referrals = entry.canExportMemberReferrals;
      }
      if (entry.canExportReports !== undefined) {
        exportFlags.can_export_reports = entry.canExportReports;
      }

      /*
       * Modes, plus the matching boolean for backward compatibility.
       *
       * `download` is the only mode that maps to `true`: both `request` and `none`
       * mean "no direct download", which is exactly what the old flag encoded.
       */
      for (const [section, mode] of Object.entries(entry.exportModes ?? {})) {
        if (!EXPORT_SECTIONS.includes(section as ExportSectionKey)) continue;
        if (!EXPORT_MODES.includes(mode as ExportMode)) continue;
        (exportFlags as any)[`export_mode_${section}`] = mode;
        // Only where the column exists — see SECTIONS_WITH_LEGACY_BOOLEAN.
        if (SECTIONS_WITH_LEGACY_BOOLEAN.has(section)) {
          (exportFlags as any)[`can_export_${section}`] = mode === "download";
        }
      }

      await Promise.all([
        ConsoleUserAccessListRepository.ReplacePromoCodeAccess(
          userId,
          entry.promoCodeIds,
        ),
        ConsoleUserAccessListRepository.ReplaceCampaignAccess(
          userId,
          entry.campaignIds,
        ),
        ...(Object.keys(exportFlags).length > 0
          ? [ConsoleUserAccessListRepository.SetExportFlags(userId, exportFlags)]
          : []),
      ]);
      return GetUserAccess(userId);
    } catch (err) {
      logError(err);
      throw new Error(
        (err as Error).message ?? "Failed saving user access list",
      );
    }
  };

  // Returns null when the user is unrestricted (super admin, or has never been
  // scoped)
  const GetPromoCodeAccessScope = async (
    userId: string,
  ): Promise<string[] | null> => {
    const user = await ConsoleUserRepository.FindEntryById(userId);
    if (user?.is_super_admin) return null;

    const allowed =
      await ConsoleUserAccessListRepository.FindPromoCodeIdsByUserId(userId);
    return allowed.length === 0 ? null : allowed;
  };

  const GetCampaignAccessScope = async (
    userId: string,
  ): Promise<string[] | null> => {
    const user = await ConsoleUserRepository.FindEntryById(userId);
    if (user?.is_super_admin) return null;

    const allowed =
      await ConsoleUserAccessListRepository.FindCampaignIdsByUserId(userId);
    return allowed.length === 0 ? null : allowed;
  };

  const FilterAllowedPromoCodeIds = async (
    userId: string,
    candidateIds: string[],
  ): Promise<string[] | null> => {
    const scope = await GetPromoCodeAccessScope(userId);
    if (scope === null) return null;
    const allowedSet = new Set(scope);
    return candidateIds.filter((id) => allowedSet.has(id));
  };

  const FindMemberUserIds = async (): Promise<string[]> => {
    return ConsoleUserAccessListRepository.FindMemberUserIds();
  };

  const AddUserToAccessList = async (userId: string, addedBy: string | null) => {
    try {
      await ConsoleUserAccessListRepository.AddMember(userId, addedBy);
    } catch (err) {
      logError(err);
      throw new Error(
        (err as Error).message ?? "Failed adding user to access list",
      );
    }
  };

  const RemoveUserFromAccessList = async (userId: string) => {
    try {
      await Promise.all([
        ConsoleUserAccessListRepository.RemoveMember(userId),
        ConsoleUserAccessListRepository.ReplacePromoCodeAccess(userId, []),
        ConsoleUserAccessListRepository.ReplaceCampaignAccess(userId, []),
      ]);
    } catch (err) {
      logError(err);
      throw new Error(
        (err as Error).message ?? "Failed removing user from access list",
      );
    }
  };

  // Export permissions only exist for members; a non-member (or nobody ever
  // scoped) always keeps the default of "can export" — these toggles can only
  // take export away from someone already on the Access List, never restrict
  // anyone else.
  const GetExportPermissions = async (userId: string) => {
    const member = await ConsoleUserAccessListRepository.FindMemberByUserId(userId);
    if (!member) {
      return {
        isMember: false,
        canExportCampaigns: true,
        canExportPromoCodes: true,
        canExportMemberReferrals: true,
        canExportReports: true,
        // Not an access-list member => unrestricted, so every section downloads.
        exportModes: Object.fromEntries(
          EXPORT_SECTIONS.map((k) => [k, "download" as ExportMode]),
        ) as Record<ExportSectionKey, ExportMode>,
      };
    }
    return {
      isMember: true,
      canExportCampaigns: member.can_export_campaigns,
      canExportPromoCodes: member.can_export_promo_codes,
      canExportMemberReferrals: member.can_export_member_referrals,
      canExportReports: member.can_export_reports,
      /*
       * Falls back to the boolean when the mode column is empty.
       *
       * Rows written before the mode migration, or by any path that only sets the
       * boolean, still resolve correctly rather than defaulting to `download` and
       * quietly handing back access. Which boolean stands in for a section is
       * declared in LEGACY_BOOLEAN_FOR, because the two newest sections have none
       * of their own.
       */
      exportModes: Object.fromEntries(
        EXPORT_SECTIONS.map((k) => [
          k,
          ((member as any)[`export_mode_${k}`] as ExportMode) ??
            ((member as any)[LEGACY_BOOLEAN_FOR[k]] === false
              ? "request"
              : "download"),
        ]),
      ) as Record<ExportSectionKey, ExportMode>,
    };
  };

  return {
    GetUserAccess,
    SetUserAccess,
    GetPromoCodeAccessScope,
    GetCampaignAccessScope,
    FilterAllowedPromoCodeIds,
    FindMemberUserIds,
    AddUserToAccessList,
    RemoveUserFromAccessList,
    GetExportPermissions,
  };
};

export type TConsoleUserAccessListServices = ReturnType<
  typeof ConsoleUserAccessListServices
>;
