import type { Kysely } from "kysely";

/**
 * Promo access roles: a named bundle of campaigns, promo codes and export modes.
 *
 * Separate from `console_roles`, which grants feature privileges. This grants data
 * access, and the two are not the same question — see the migration for why they
 * were split.
 *
 * These tables postdate the last kysely-codegen run, so this works against an
 * untyped `Kysely<any>`, as the other newer repositories do.
 */

export const ROLE_SECTIONS = [
  "campaigns",
  "promo_codes",
  "member_referrals",
  "reports",
  "bookings",
] as const;
export type RoleSection = (typeof ROLE_SECTIONS)[number];

export const ROLE_MODES = ["download", "request", "none"] as const;
export type RoleMode = (typeof ROLE_MODES)[number];

export interface PromoAccessRoleSummary {
  id: string;
  name: string;
  description: string | null;
  created_at: string;
  campaign_count: number;
  promo_code_count: number;
}

export interface PromoAccessRole extends PromoAccessRoleSummary {
  exportModes: Record<RoleSection, RoleMode>;
  campaignIds: string[];
  promoCodeIds: string[];
}

function defaultModes(): Record<RoleSection, RoleMode> {
  return Object.fromEntries(
    ROLE_SECTIONS.map((s) => [s, "download" as RoleMode]),
  ) as Record<RoleSection, RoleMode>;
}

export class PromoAccessRolesRepository {
  private db: Kysely<any>;

  constructor(db: unknown) {
    this.db = db as Kysely<any>;
  }

  /** All roles with their counts, for the list view. */
  async list(): Promise<PromoAccessRoleSummary[]> {
    const rows = await this.db
      .selectFrom("promo_access_roles as r")
      .select([
        "r.id",
        "r.name",
        "r.description",
        "r.created_at",
        // Counted via subqueries rather than joins: two joins would multiply the
        // rows and inflate both counts.
        (eb: any) =>
          eb
            .selectFrom("promo_access_role_campaigns as c")
            .select((e: any) => e.fn.countAll().as("n"))
            .whereRef("c.promo_access_role_id", "=", "r.id")
            .as("campaign_count"),
        (eb: any) =>
          eb
            .selectFrom("promo_access_role_promo_codes as p")
            .select((e: any) => e.fn.countAll().as("n"))
            .whereRef("p.promo_access_role_id", "=", "r.id")
            .as("promo_code_count"),
      ])
      .orderBy("r.name", "asc")
      .execute();

    return (rows as any[]).map((r) => ({
      id: String(r.id),
      name: String(r.name),
      description: r.description ?? null,
      created_at: String(r.created_at),
      campaign_count: Number(r.campaign_count ?? 0),
      promo_code_count: Number(r.promo_code_count ?? 0),
    }));
  }

  async get(id: string): Promise<PromoAccessRole | null> {
    const role = await this.db
      .selectFrom("promo_access_roles")
      .select(["id", "name", "description", "created_at"])
      .where("id", "=", id)
      .executeTakeFirst();
    if (!role) return null;

    const [modes, campaigns, codes] = await Promise.all([
      this.db
        .selectFrom("promo_access_role_export_modes")
        .select(["section", "mode"])
        .where("promo_access_role_id", "=", id)
        .execute(),
      this.db
        .selectFrom("promo_access_role_campaigns")
        .select("campaign_id")
        .where("promo_access_role_id", "=", id)
        .execute(),
      this.db
        .selectFrom("promo_access_role_promo_codes")
        .select("promo_code_id")
        .where("promo_access_role_id", "=", id)
        .execute(),
    ]);

    const exportModes = defaultModes();
    for (const row of modes as Array<{ section: string; mode: string }>) {
      if ((ROLE_SECTIONS as readonly string[]).includes(row.section)) {
        exportModes[row.section as RoleSection] = row.mode as RoleMode;
      }
    }

    return {
      id: String((role as any).id),
      name: String((role as any).name),
      description: (role as any).description ?? null,
      created_at: String((role as any).created_at),
      campaign_count: (campaigns as any[]).length,
      promo_code_count: (codes as any[]).length,
      exportModes,
      campaignIds: (campaigns as any[]).map((r) => String(r.campaign_id)),
      promoCodeIds: (codes as any[]).map((r) => String(r.promo_code_id)),
    };
  }

  async create(input: {
    name: string;
    description: string | null;
    createdBy: string | null;
  }): Promise<PromoAccessRole> {
    const row = await this.db
      .insertInto("promo_access_roles")
      .values({
        name: input.name,
        description: input.description,
        created_by: input.createdBy,
      })
      .returning(["id"])
      .executeTakeFirstOrThrow();
    return (await this.get(String((row as any).id)))!;
  }

  async rename(
    id: string,
    input: { name?: string; description?: string | null },
  ): Promise<void> {
    const set: Record<string, unknown> = { updated_at: new Date() };
    if (input.name !== undefined) set.name = input.name;
    if (input.description !== undefined) set.description = input.description;
    await this.db
      .updateTable("promo_access_roles")
      .set(set)
      .where("id", "=", id)
      .execute();
  }

  /**
   * Replaces the role's access sets.
   *
   * Delete-then-insert per table: the payload is the complete desired state, and
   * these sets are small enough that diffing would add risk for no measurable gain.
   */
  async replaceAccess(
    id: string,
    template: {
      exportModes?: Record<string, string>;
      campaignIds?: string[];
      promoCodeIds?: string[];
    },
  ): Promise<PromoAccessRole | null> {
    if (template.exportModes) {
      const rows = Object.entries(template.exportModes)
        .filter(
          ([section, mode]) =>
            (ROLE_SECTIONS as readonly string[]).includes(section) &&
            (ROLE_MODES as readonly string[]).includes(mode),
        )
        .map(([section, mode]) => ({
          promo_access_role_id: id,
          section,
          mode,
        }));
      await this.db
        .deleteFrom("promo_access_role_export_modes")
        .where("promo_access_role_id", "=", id)
        .execute();
      if (rows.length > 0) {
        await this.db
          .insertInto("promo_access_role_export_modes")
          .values(rows)
          .execute();
      }
    }

    if (template.campaignIds) {
      await this.db
        .deleteFrom("promo_access_role_campaigns")
        .where("promo_access_role_id", "=", id)
        .execute();
      const ids = Array.from(new Set(template.campaignIds.filter(Boolean)));
      if (ids.length > 0) {
        await this.db
          .insertInto("promo_access_role_campaigns")
          .values(
            ids.map((campaign_id) => ({
              promo_access_role_id: id,
              campaign_id,
            })),
          )
          .execute();
      }
    }

    if (template.promoCodeIds) {
      await this.db
        .deleteFrom("promo_access_role_promo_codes")
        .where("promo_access_role_id", "=", id)
        .execute();
      const ids = Array.from(new Set(template.promoCodeIds.filter(Boolean)));
      if (ids.length > 0) {
        await this.db
          .insertInto("promo_access_role_promo_codes")
          .values(
            ids.map((promo_code_id) => ({
              promo_access_role_id: id,
              promo_code_id,
            })),
          )
          .execute();
      }
    }

    return this.get(id);
  }

  /** Removes the role and everything attached to it. */
  async remove(id: string): Promise<void> {
    // No FK cascade (the ids are plain columns), so children go first.
    await this.db
      .deleteFrom("promo_access_role_export_modes")
      .where("promo_access_role_id", "=", id)
      .execute();
    await this.db
      .deleteFrom("promo_access_role_campaigns")
      .where("promo_access_role_id", "=", id)
      .execute();
    await this.db
      .deleteFrom("promo_access_role_promo_codes")
      .where("promo_access_role_id", "=", id)
      .execute();
    await this.db
      .deleteFrom("promo_access_roles")
      .where("id", "=", id)
      .execute();
  }
}
