import { and, asc, eq, inArray, isNull } from 'drizzle-orm';
import {
  crmRoleProfiles,
  crmUserRoleAssignments,
  memberships,
  msOptions,
  users
} from '@/db/schema';
import { auditAction } from '@/features/foundation/audit-log/service';
import { getUserBranches } from '@/features/foundation/branch-scope/service';
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
import { ensureCrmRoleProfiles } from '@/features/foundation/role-management/server/service';
import {
  ROLE_ASSIGNMENT_SCOPE_MODES,
  isRoleAssignmentScopeMode,
  type CrmRoleAssignmentScopeMode
} from '@/lib/auth/rbac';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';

function normalizeStringArray(value: unknown) {
  return Array.isArray(value)
    ? [
        ...new Set(
          value.filter(
            (item): item is string => typeof item === 'string' && item.trim().length > 0
          )
        )
      ]
    : [];
}

function isSelectedScopeInvalid(mode: CrmRoleAssignmentScopeMode, values: string[]) {
  return mode === 'selected' && values.length === 0;
}

function mapAssignmentRow(
  row: typeof crmUserRoleAssignments.$inferSelect,
  options: {
    roleProfileCode: string;
    roleProfileName: string;
    userName: string;
    userEmail: string;
  }
) {
  return {
    id: row.id,
    organizationId: row.organizationId,
    userId: row.userId,
    userName: options.userName,
    userEmail: options.userEmail,
    roleProfileId: row.roleProfileId,
    roleProfileCode: options.roleProfileCode,
    roleProfileName: options.roleProfileName,
    branchScopeMode: isRoleAssignmentScopeMode(row.branchScopeMode)
      ? row.branchScopeMode
      : 'inherit',
    branchScopeIds: normalizeStringArray(row.branchScopeIds),
    productTypeScopeMode: isRoleAssignmentScopeMode(row.productTypeScopeMode)
      ? row.productTypeScopeMode
      : 'inherit',
    productTypeScopeIds: normalizeStringArray(row.productTypeScopeIds),
    isPrimary: row.isPrimary,
    isActive: row.isActive,
    assignedBy: row.assignedBy,
    assignedAt: row.assignedAt.toISOString(),
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

async function ensurePrimaryAssignmentForUser(organizationId: string, userId: string) {
  const activeAssignments = await db
    .select({
      id: crmUserRoleAssignments.id,
      isPrimary: crmUserRoleAssignments.isPrimary
    })
    .from(crmUserRoleAssignments)
    .where(
      and(
        eq(crmUserRoleAssignments.organizationId, organizationId),
        eq(crmUserRoleAssignments.userId, userId),
        eq(crmUserRoleAssignments.isActive, true),
        isNull(crmUserRoleAssignments.deletedAt)
      )
    )
    .orderBy(asc(crmUserRoleAssignments.assignedAt));

  if (!activeAssignments.length || activeAssignments.some((item) => item.isPrimary)) {
    return;
  }

  await db
    .update(crmUserRoleAssignments)
    .set({
      isPrimary: true,
      updatedAt: new Date()
    })
    .where(eq(crmUserRoleAssignments.id, activeAssignments[0]!.id));
}

async function setPrimaryAssignment(
  organizationId: string,
  userId: string,
  assignmentId: string,
  isPrimary: boolean
) {
  if (!isPrimary) {
    return;
  }

  await db
    .update(crmUserRoleAssignments)
    .set({
      isPrimary: false,
      updatedAt: new Date()
    })
    .where(
      and(
        eq(crmUserRoleAssignments.organizationId, organizationId),
        eq(crmUserRoleAssignments.userId, userId),
        isNull(crmUserRoleAssignments.deletedAt)
      )
    );

  await db
    .update(crmUserRoleAssignments)
    .set({
      isPrimary: true,
      updatedAt: new Date()
    })
    .where(eq(crmUserRoleAssignments.id, assignmentId));
}

export async function ensureCrmRoleAssignmentsBackfillForMembership(
  organizationId: string,
  membership: typeof memberships.$inferSelect,
  actorUserId: string
) {
  await ensureCrmRoleProfiles(organizationId, actorUserId);

  const [roleProfile] = await db
    .select({
      id: crmRoleProfiles.id
    })
    .from(crmRoleProfiles)
    .where(
      and(
        eq(crmRoleProfiles.organizationId, organizationId),
        eq(crmRoleProfiles.code, membership.businessRole),
        isNull(crmRoleProfiles.deletedAt)
      )
    )
    .limit(1);

  if (!roleProfile) {
    return;
  }

  const existing = await db.query.crmUserRoleAssignments.findFirst({
    where: and(
      eq(crmUserRoleAssignments.organizationId, organizationId),
      eq(crmUserRoleAssignments.userId, membership.userId),
      eq(crmUserRoleAssignments.roleProfileId, roleProfile.id)
    )
  });

  const branchScopeIds = normalizeStringArray(membership.branchScopeIds);
  const productTypeScopeIds = normalizeStringArray(membership.productTypeScopeIds);
  const branchScopeMode: CrmRoleAssignmentScopeMode = branchScopeIds.length ? 'selected' : 'all';
  const productTypeScopeMode: CrmRoleAssignmentScopeMode = productTypeScopeIds.length
    ? 'selected'
    : 'all';

  if (existing) {
    if (existing.deletedAt || !existing.isActive) {
      await db
        .update(crmUserRoleAssignments)
        .set({
          branchScopeMode,
          branchScopeIds,
          productTypeScopeMode,
          productTypeScopeIds,
          isPrimary: true,
          isActive: true,
          assignedBy: actorUserId,
          assignedAt: new Date(),
          updatedAt: new Date(),
          deletedAt: null
        })
        .where(eq(crmUserRoleAssignments.id, existing.id));
    }

    await setPrimaryAssignment(organizationId, membership.userId, existing.id, true);
    return;
  }

  const [created] = await db
    .insert(crmUserRoleAssignments)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      userId: membership.userId,
      roleProfileId: roleProfile.id,
      branchScopeMode,
      branchScopeIds,
      productTypeScopeMode,
      productTypeScopeIds,
      isPrimary: true,
      isActive: true,
      assignedBy: actorUserId
    })
    .returning();

  await setPrimaryAssignment(organizationId, membership.userId, created.id, true);
}

export async function ensureCrmRoleAssignmentsBackfillForOrganization(
  organizationId: string,
  actorUserId: string
) {
  const membershipRows = await db.query.memberships.findMany({
    where: eq(memberships.organizationId, organizationId)
  });

  for (const membership of membershipRows) {
    await ensureCrmRoleAssignmentsBackfillForMembership(organizationId, membership, actorUserId);
  }
}

export async function listResolvedCrmRoleAssignments(
  organizationId: string,
  actorUserId: string
) {
  await ensureCrmRoleAssignmentsBackfillForOrganization(organizationId, actorUserId);

  const rows = await db
    .select({
      assignment: crmUserRoleAssignments,
      roleProfileCode: crmRoleProfiles.code,
      roleProfileName: crmRoleProfiles.name,
      userName: users.name,
      userEmail: users.email
    })
    .from(crmUserRoleAssignments)
    .innerJoin(crmRoleProfiles, eq(crmRoleProfiles.id, crmUserRoleAssignments.roleProfileId))
    .innerJoin(users, eq(users.id, crmUserRoleAssignments.userId))
    .where(
      and(
        eq(crmUserRoleAssignments.organizationId, organizationId),
        isNull(crmUserRoleAssignments.deletedAt)
      )
    )
    .orderBy(asc(users.name), asc(crmUserRoleAssignments.assignedAt));

  const [branchOptions, productTypeOptions, roleProfiles, orgUsers] = await Promise.all([
    getUserBranches(),
    getActiveOptionsByCategory('crm_product_type', { organizationId }),
    db
      .select({
        id: crmRoleProfiles.id,
        code: crmRoleProfiles.code,
        name: crmRoleProfiles.name
      })
      .from(crmRoleProfiles)
      .where(
        and(
          eq(crmRoleProfiles.organizationId, organizationId),
          eq(crmRoleProfiles.isActive, true),
          isNull(crmRoleProfiles.deletedAt)
        )
      )
      .orderBy(asc(crmRoleProfiles.name)),
    db
      .select({
        userId: users.id,
        name: users.name,
        email: users.email
      })
      .from(memberships)
      .innerJoin(users, eq(users.id, memberships.userId))
      .where(eq(memberships.organizationId, organizationId))
      .orderBy(asc(users.name))
  ]);

  return {
    assignments: rows.map((row) =>
      mapAssignmentRow(row.assignment, {
        roleProfileCode: row.roleProfileCode,
        roleProfileName: row.roleProfileName,
        userName: row.userName,
        userEmail: row.userEmail
      })
    ),
    scopeModes: [...ROLE_ASSIGNMENT_SCOPE_MODES],
    roleProfiles,
    users: [...new Map(orgUsers.map((row) => [row.userId, row])).values()].map((row) => ({
      id: row.userId,
      name: row.name,
      email: row.email
    })),
    branches: branchOptions.map((branch) => ({
      id: branch.id,
      code: branch.code,
      label: branch.name
    })),
    productTypes: productTypeOptions.map((option) => ({
      id: option.id,
      code: option.code,
      label: option.label
    }))
  };
}

export interface UpsertCrmUserRoleAssignmentPayload {
  userId: string;
  roleProfileId: string;
  branchScopeMode: CrmRoleAssignmentScopeMode;
  branchScopeIds: string[];
  productTypeScopeMode: CrmRoleAssignmentScopeMode;
  productTypeScopeIds: string[];
  isPrimary: boolean;
  isActive: boolean;
}

async function validateAssignmentPayload(
  organizationId: string,
  payload: UpsertCrmUserRoleAssignmentPayload
) {
  const [userMembership, roleProfile] = await Promise.all([
    db.query.memberships.findFirst({
      where: and(
        eq(memberships.organizationId, organizationId),
        eq(memberships.userId, payload.userId)
      )
    }),
    db.query.crmRoleProfiles.findFirst({
      where: and(
        eq(crmRoleProfiles.organizationId, organizationId),
        eq(crmRoleProfiles.id, payload.roleProfileId),
        isNull(crmRoleProfiles.deletedAt)
      )
    })
  ]);

  if (!userMembership) {
    throw new AuthError('User does not belong to this organization', 404);
  }

  if (!roleProfile) {
    throw new AuthError('CRM role profile not found', 404);
  }

  if (isSelectedScopeInvalid(payload.branchScopeMode, payload.branchScopeIds)) {
    throw new AuthError('Select at least one branch when branch scope mode is selected', 400);
  }

  if (isSelectedScopeInvalid(payload.productTypeScopeMode, payload.productTypeScopeIds)) {
    throw new AuthError(
      'Select at least one product type when product scope mode is selected',
      400
    );
  }

  const branches = await getUserBranches();
  const validBranchIds = new Set(branches.map((branch) => branch.id));
  const productTypes = await getActiveOptionsByCategory('crm_product_type', { organizationId });
  const validProductTypeIds = new Set(productTypes.map((option) => option.id));

  if (payload.branchScopeIds.some((id) => !validBranchIds.has(id))) {
    throw new AuthError('Invalid branch scope selection', 400);
  }

  if (payload.productTypeScopeIds.some((id) => !validProductTypeIds.has(id))) {
    throw new AuthError('Invalid product type scope selection', 400);
  }

  return {
    userMembership,
    roleProfile
  };
}

export async function createCrmUserRoleAssignment(
  organizationId: string,
  actorUserId: string,
  payload: UpsertCrmUserRoleAssignmentPayload
) {
  await validateAssignmentPayload(organizationId, payload);

  const duplicate = await db.query.crmUserRoleAssignments.findFirst({
    where: and(
      eq(crmUserRoleAssignments.organizationId, organizationId),
      eq(crmUserRoleAssignments.userId, payload.userId),
      eq(crmUserRoleAssignments.roleProfileId, payload.roleProfileId)
    )
  });

  let assignmentId = duplicate?.id ?? null;

  if (duplicate) {
    await db
      .update(crmUserRoleAssignments)
      .set({
        branchScopeMode: payload.branchScopeMode,
        branchScopeIds: payload.branchScopeIds,
        productTypeScopeMode: payload.productTypeScopeMode,
        productTypeScopeIds: payload.productTypeScopeIds,
        isPrimary: payload.isPrimary,
        isActive: payload.isActive,
        assignedBy: actorUserId,
        assignedAt: new Date(),
        updatedAt: new Date(),
        deletedAt: null
      })
      .where(eq(crmUserRoleAssignments.id, duplicate.id));
  } else {
    const [created] = await db
      .insert(crmUserRoleAssignments)
      .values({
        id: crypto.randomUUID(),
        organizationId,
        userId: payload.userId,
        roleProfileId: payload.roleProfileId,
        branchScopeMode: payload.branchScopeMode,
        branchScopeIds: payload.branchScopeIds,
        productTypeScopeMode: payload.productTypeScopeMode,
        productTypeScopeIds: payload.productTypeScopeIds,
        isPrimary: payload.isPrimary,
        isActive: payload.isActive,
        assignedBy: actorUserId
      })
      .returning();

    assignmentId = created.id;
  }

  if (!assignmentId) {
    throw new AuthError('Unable to create CRM role assignment', 500);
  }

  await setPrimaryAssignment(organizationId, payload.userId, assignmentId, payload.isPrimary);
  await ensurePrimaryAssignmentForUser(organizationId, payload.userId);

  const refreshed = await listResolvedCrmRoleAssignments(organizationId, actorUserId);
  const assignment = refreshed.assignments.find((item) => item.id === assignmentId);

  if (!assignment) {
    throw new AuthError('CRM role assignment not found after create', 500);
  }

  await auditAction({
    organizationId,
    userId: actorUserId,
    entityType: 'crm_user_role_assignment',
    entityId: assignment.id,
    action: duplicate ? 'reactivate' : 'create',
    afterData: assignment
  });

  return assignment;
}

export async function updateCrmUserRoleAssignment(
  organizationId: string,
  actorUserId: string,
  assignmentId: string,
  payload: Omit<UpsertCrmUserRoleAssignmentPayload, 'userId' | 'roleProfileId'>
) {
  const current = await db.query.crmUserRoleAssignments.findFirst({
    where: and(
      eq(crmUserRoleAssignments.id, assignmentId),
      eq(crmUserRoleAssignments.organizationId, organizationId)
    )
  });

  if (!current || current.deletedAt) {
    throw new AuthError('CRM role assignment not found', 404);
  }

  await validateAssignmentPayload(organizationId, {
    userId: current.userId,
    roleProfileId: current.roleProfileId,
    ...payload
  });

  await db
    .update(crmUserRoleAssignments)
    .set({
      branchScopeMode: payload.branchScopeMode,
      branchScopeIds: payload.branchScopeIds,
      productTypeScopeMode: payload.productTypeScopeMode,
      productTypeScopeIds: payload.productTypeScopeIds,
      isPrimary: payload.isPrimary,
      isActive: payload.isActive,
      updatedAt: new Date()
    })
    .where(eq(crmUserRoleAssignments.id, assignmentId));

  await setPrimaryAssignment(organizationId, current.userId, assignmentId, payload.isPrimary);
  await ensurePrimaryAssignmentForUser(organizationId, current.userId);

  const refreshed = await listResolvedCrmRoleAssignments(organizationId, actorUserId);
  const assignment = refreshed.assignments.find((item) => item.id === assignmentId);

  if (!assignment) {
    throw new AuthError('CRM role assignment not found after update', 500);
  }

  await auditAction({
    organizationId,
    userId: actorUserId,
    entityType: 'crm_user_role_assignment',
    entityId: assignment.id,
    action: payload.isActive ? 'update' : 'deactivate',
    beforeData: current,
    afterData: assignment
  });

  return assignment;
}

export async function deleteCrmUserRoleAssignment(
  organizationId: string,
  actorUserId: string,
  assignmentId: string
) {
  const current = await db.query.crmUserRoleAssignments.findFirst({
    where: and(
      eq(crmUserRoleAssignments.id, assignmentId),
      eq(crmUserRoleAssignments.organizationId, organizationId)
    )
  });

  if (!current || current.deletedAt) {
    throw new AuthError('CRM role assignment not found', 404);
  }

  await db
    .update(crmUserRoleAssignments)
    .set({
      isActive: false,
      isPrimary: false,
      deletedAt: new Date(),
      updatedAt: new Date()
    })
    .where(eq(crmUserRoleAssignments.id, assignmentId));

  await ensurePrimaryAssignmentForUser(organizationId, current.userId);

  await auditAction({
    organizationId,
    userId: actorUserId,
    entityType: 'crm_user_role_assignment',
    entityId: assignmentId,
    action: 'delete',
    beforeData: current
  });
}

export async function listCrmRoleAssignmentsForUser(
  organizationId: string,
  actorUserId: string,
  userId: string
) {
  const data = await listResolvedCrmRoleAssignments(organizationId, actorUserId);

  return data.assignments.filter((assignment) => assignment.userId === userId);
}

export function parseRoleAssignmentScopeMode(value: string): CrmRoleAssignmentScopeMode {
  if (!isRoleAssignmentScopeMode(value)) {
    throw new AuthError('Invalid role assignment scope mode', 400);
  }

  return value;
}

export async function getRoleAssignmentReferenceSummaries(organizationId: string) {
  const [roleProfiles, usersInOrganization, branches, productTypes] = await Promise.all([
    db
      .select({
        id: crmRoleProfiles.id,
        code: crmRoleProfiles.code,
        name: crmRoleProfiles.name
      })
      .from(crmRoleProfiles)
      .where(
        and(
          eq(crmRoleProfiles.organizationId, organizationId),
          eq(crmRoleProfiles.isActive, true),
          isNull(crmRoleProfiles.deletedAt)
        )
      )
      .orderBy(asc(crmRoleProfiles.name)),
    db
      .select({
        id: users.id,
        name: users.name,
        email: users.email
      })
      .from(memberships)
      .innerJoin(users, eq(users.id, memberships.userId))
      .where(eq(memberships.organizationId, organizationId))
      .orderBy(asc(users.name)),
    getUserBranches(),
    getActiveOptionsByCategory('crm_product_type', { organizationId })
  ]);

  return {
    roleProfiles,
    users: [...new Map(usersInOrganization.map((row) => [row.id, row])).values()],
    branches: branches.map((branch) => ({
      id: branch.id,
      code: branch.code,
      label: branch.name
    })),
    productTypes: productTypes.map((option) => ({
      id: option.id,
      code: option.code,
      label: option.label
    }))
  };
}
