import { logError } from "@/lib/logger";
import { error as errorResponse, success } from "@/lib/response";
import { PromoAccessRolesRepository } from "@/internal/repository/admin_console/promo_access_roles";
import { ConsoleUserAccessListServices } from "@/internal/console-user-access-list/console-user-access-list.services";
import { ConsoleUserAccessListRepository } from "@/internal/console-user-access-list/console-user-access-list.repository";
import { ConsoleUserRepository } from "@/internal/console-users/console-users.repository";
import type { Context } from "hono";

/**
 * Promo access roles.
 *
 * Create a named bundle of campaigns, promo codes and export modes, then apply it
 * to a member. Applying copies the settings onto that member's own access list,
 * which stays editable — so one member can be adjusted afterwards without touching
 * anyone else who shares the role.
 */

function repo(c: Context) {
  return new PromoAccessRolesRepository(c.get("datastore"));
}

// The access-list service is a factory over its repositories, as elsewhere.
function accessListServices(c: Context) {
  return ConsoleUserAccessListServices({
    ConsoleUserAccessListRepository: ConsoleUserAccessListRepository(c as any),
    ConsoleUserRepository: ConsoleUserRepository(c as any),
  });
}

export const listPromoAccessRolesHandler = async (c: Context) => {
  try {
    return success(c, await repo(c).list(), "Promo access roles fetched", 200);
  } catch (err: any) {
    logError("Error in listPromoAccessRolesHandler:", err);
    return errorResponse(c, err?.message || "Failed to list roles", 500);
  }
};

export const getPromoAccessRoleHandler = async (c: Context) => {
  try {
    const id = c.req.param("id");
    if (!id) return errorResponse(c, "Role id is required", 400);
    const role = await repo(c).get(id);
    if (!role) return errorResponse(c, "Role not found", 404);
    return success(c, role, "Promo access role fetched", 200);
  } catch (err: any) {
    logError("Error in getPromoAccessRoleHandler:", err);
    return errorResponse(c, err?.message || "Failed to fetch role", 500);
  }
};

export const createPromoAccessRoleHandler = async (c: Context) => {
  try {
    const body = (await c.req.json().catch(() => null)) as {
      name?: string;
      description?: string;
      exportModes?: Record<string, string>;
      campaignIds?: string[];
      promoCodeIds?: string[];
    } | null;
    const name = String(body?.name ?? "").trim();
    if (!name) return errorResponse(c, "A role name is required", 400);

    const created = await repo(c).create({
      name,
      description: body?.description ? String(body.description) : null,
      createdBy: String(c.get("adminEmail") ?? "") || null,
    });

    // Access is optional at creation, so a role can be named first and filled in
    // later without a second required step.
    if (body?.exportModes || body?.campaignIds || body?.promoCodeIds) {
      const withAccess = await repo(c).replaceAccess(created.id, {
        exportModes: body.exportModes,
        campaignIds: body.campaignIds,
        promoCodeIds: body.promoCodeIds,
      });
      return success(c, withAccess, "Promo access role created", 201);
    }
    return success(c, created, "Promo access role created", 201);
  } catch (err: any) {
    logError("Error in createPromoAccessRoleHandler:", err);
    // The unique index on lower(name) is what enforces this.
    if (String(err?.message ?? "").includes("uq_promo_access_roles_name")) {
      return errorResponse(c, "A role with that name already exists", 409);
    }
    return errorResponse(c, err?.message || "Failed to create role", 500);
  }
};

export const updatePromoAccessRoleHandler = async (c: Context) => {
  try {
    const id = c.req.param("id");
    if (!id) return errorResponse(c, "Role id is required", 400);
    const body = (await c.req.json().catch(() => null)) as {
      name?: string;
      description?: string | null;
      exportModes?: Record<string, string>;
      campaignIds?: string[];
      promoCodeIds?: string[];
    } | null;
    if (!body) return errorResponse(c, "Expected a JSON body", 400);

    if (body.name !== undefined || body.description !== undefined) {
      await repo(c).rename(id, {
        name: body.name ? String(body.name).trim() : undefined,
        description: body.description ?? undefined,
      });
    }
    const updated = await repo(c).replaceAccess(id, {
      exportModes: body.exportModes,
      campaignIds: Array.isArray(body.campaignIds) ? body.campaignIds : undefined,
      promoCodeIds: Array.isArray(body.promoCodeIds) ? body.promoCodeIds : undefined,
    });
    if (!updated) return errorResponse(c, "Role not found", 404);
    return success(c, updated, "Promo access role saved", 200);
  } catch (err: any) {
    logError("Error in updatePromoAccessRoleHandler:", err);
    return errorResponse(c, err?.message || "Failed to save role", 500);
  }
};

export const deletePromoAccessRoleHandler = async (c: Context) => {
  try {
    const id = c.req.param("id");
    if (!id) return errorResponse(c, "Role id is required", 400);
    await repo(c).remove(id);
    /*
     * Members already configured from this role keep their access.
     *
     * Applying is a copy, so their own rows are unaffected — deleting the role
     * removes the shortcut, not anyone's access. Cascading would silently revoke
     * access from people who may have been adjusted individually since.
     */
    return success(c, { ok: true }, "Promo access role deleted", 200);
  } catch (err: any) {
    logError("Error in deletePromoAccessRoleHandler:", err);
    return errorResponse(c, err?.message || "Failed to delete role", 500);
  }
};

/**
 * Applies a role to a member.
 *
 * `merge` (default) unions the campaigns and codes with what the member already
 * has and keeps the more permissive export mode, so applying can only widen access.
 * `replace` overwrites — the only path that can take access away.
 */
export const applyPromoAccessRoleHandler = async (c: Context) => {
  try {
    const userId = c.req.param("userId");
    const body = (await c.req.json().catch(() => null)) as {
      roleId?: string;
      mode?: string;
    } | null;
    const roleId = String(body?.roleId ?? "");
    if (!userId || !roleId) {
      return errorResponse(c, "A user id and role id are required", 400);
    }
    const role = await repo(c).get(roleId);
    if (!role) return errorResponse(c, "Role not found", 404);

    const replace = body?.mode === "replace";
    let campaignIds = role.campaignIds;
    let promoCodeIds = role.promoCodeIds;
    const exportModes: Record<string, string> = { ...role.exportModes };

    if (!replace) {
      const svc = accessListServices(c);
      // The selections and the export modes come from two different calls.
      const [current, perms] = await Promise.all([
        svc.GetUserAccess(userId),
        svc.GetExportPermissions(userId),
      ]);
      campaignIds = Array.from(
        new Set([...(current?.campaignIds ?? []), ...campaignIds]),
      );
      promoCodeIds = Array.from(
        new Set([...(current?.promoCodeIds ?? []), ...promoCodeIds]),
      );
      const RANK: Record<string, number> = { none: 0, request: 1, download: 2 };
      const existing = (perms as any)?.exportModes ?? {};
      for (const [section, mode] of Object.entries(exportModes)) {
        const have = existing[section];
        if (have && (RANK[have] ?? 0) > (RANK[mode] ?? 0)) {
          exportModes[section] = have;
        }
      }
    }

    const updated = await accessListServices(c).SetUserAccess(userId, {
      promoCodeIds,
      campaignIds,
      exportModes,
    });
    return success(
      c,
      updated,
      replace
        ? `Member access replaced from "${role.name}"`
        : `"${role.name}" access added to this member`,
      200,
    );
  } catch (err: any) {
    logError("Error in applyPromoAccessRoleHandler:", err);
    return errorResponse(c, err?.message || "Failed to apply role", 500);
  }
};
