import {
  and,
  asc,
  count,
  desc,
  eq,
  gt,
  ilike,
  inArray,
  isNull,
  or
} from 'drizzle-orm';
import {
  departments,
  memberships,
  permissionTemplates,
  positions,
  userPermissionOverrides,
  userPermissionTemplateAssignments,
  users
} from '@/db/schema';
import { resolveEffectivePermissions } from '@/lib/auth/authorization.server';
import { normalizePermissions, type Permission } from '@/lib/auth/permissions';
import { db } from '@/lib/db';
import type {
  PermissionTemplateOption,
  UserPermissionAssignmentDetail,
  UserPermissionAssignmentFilters,
  UserPermissionAssignmentMutationPayload,
  UserPermissionOverrideMutationPayload
} from '../api/types';
import {
  logPermissionAuditEvent,
  PERMISSION_AUDIT_ACTION,
  toPermissionAuditValue
} from './permission-audit-service';

const sortMap = {
  employee_code: users.employeeCode,
  created_at: users.createdAt,
  full_name: users.name,
  department_name: departments.name,
  position_name: positions.name,
  updated_at: users.updatedAt
} as const;

function normalizeText(value?: string | null) {
  const normalized = value?.trim();
  return normalized ? normalized : null;
}

function serializeOverride(row: {
  id: number;
  permission: string;
  effect: 'allow' | 'deny';
  reason: string | null;
  expiresAt: Date | null;
  isActive: boolean;
  createdAt: Date;
}) {
  return {
    id: row.id,
    permission: row.permission as Permission,
    effect: row.effect,
    reason: row.reason,
    expires_at: row.expiresAt ? row.expiresAt.toISOString() : null,
    is_active: row.isActive,
    created_at: row.createdAt.toISOString()
  };
}

async function getTemplateOptions(organizationId: string): Promise<PermissionTemplateOption[]> {
  const templates = await db
    .select({
      id: permissionTemplates.id,
      name: permissionTemplates.name,
      code: permissionTemplates.code,
      isDefault: permissionTemplates.isDefault
    })
    .from(permissionTemplates)
    .where(
      and(
        eq(permissionTemplates.organizationId, organizationId),
        eq(permissionTemplates.isActive, true),
        isNull(permissionTemplates.deletedAt)
      )
    )
    .orderBy(asc(permissionTemplates.name));

  const templateIds = templates.map((template) => template.id);
  const templatePermissionRows = templateIds.length
    ? await db.query.permissionTemplatePermissions.findMany({
        where: (table, { inArray: inArrayFn }) => inArrayFn(table.templateId, templateIds)
      })
    : [];

  const permissionsByTemplate = new Map<number, Permission[]>();
  for (const row of templatePermissionRows) {
    const current = permissionsByTemplate.get(row.templateId) ?? [];
    permissionsByTemplate.set(
      row.templateId,
      normalizePermissions([...current, row.permission])
    );
  }

  return templates.map((template) => ({
    id: template.id,
    name: template.name,
    code: template.code,
    is_default: template.isDefault,
    permissions: permissionsByTemplate.get(template.id) ?? []
  }));
}

export async function listUserPermissionAssignmentsForOrganization({
  organizationId,
  filters
}: {
  organizationId: string;
  filters: UserPermissionAssignmentFilters;
}) {
  const page = Number(filters.page ?? 1);
  const limit = Number(filters.limit ?? 10);
  const search = normalizeText(filters.search);

  const clauses = [
    eq(memberships.organizationId, organizationId),
    ...(search
      ? [
          or(
            ilike(users.name, `%${search}%`),
            ilike(users.email, `%${search}%`),
            ilike(users.employeeCode, `%${search}%`),
            ilike(departments.name, `%${search}%`),
            ilike(positions.name, `%${search}%`)
          )!
        ]
      : [])
  ];

  const where = clauses.length > 1 ? and(...clauses) : clauses[0];
  let orderByClause = desc(users.createdAt);

  if (filters.sort) {
    try {
      const sortItems = JSON.parse(filters.sort) as Array<{
        id: keyof typeof sortMap;
        desc: boolean;
      }>;
      const primarySort = sortItems[0];

      if (primarySort?.id && sortMap[primarySort.id]) {
        orderByClause = primarySort.desc
          ? desc(sortMap[primarySort.id])
          : asc(sortMap[primarySort.id]);
      }
    } catch {
      orderByClause = desc(users.createdAt);
    }
  }

  const baseQuery = db
    .select({
      userId: users.id,
      employeeCode: users.employeeCode,
      fullName: users.name,
      email: users.email,
      departmentName: departments.name,
      positionName: positions.name,
      updatedAt: users.updatedAt
    })
    .from(memberships)
    .innerJoin(users, eq(users.id, memberships.userId))
    .leftJoin(departments, eq(users.departmentId, departments.id))
    .leftJoin(positions, eq(users.positionId, positions.id))
    .where(where);

  const countSubquery = baseQuery.as('scoped_user_permissions');
  const [totalResult] = await db.select({ value: count() }).from(countSubquery);
  const offset = (page - 1) * limit;
  const rows = await baseQuery.orderBy(orderByClause).limit(limit).offset(offset);

  const userIds = rows.map((row) => row.userId);
  const assignmentRows = userIds.length
    ? await db
        .select({
          userId: userPermissionTemplateAssignments.userId,
          templateId: userPermissionTemplateAssignments.templateId,
          templateName: permissionTemplates.name
        })
        .from(userPermissionTemplateAssignments)
        .innerJoin(
          permissionTemplates,
          eq(permissionTemplates.id, userPermissionTemplateAssignments.templateId)
        )
        .where(
          and(
            inArray(userPermissionTemplateAssignments.userId, userIds),
            eq(userPermissionTemplateAssignments.organizationId, organizationId),
            eq(userPermissionTemplateAssignments.isActive, true)
          )
        )
    : [];

  const overrideCountRows = userIds.length
    ? await db
        .select({
          userId: userPermissionOverrides.userId,
          value: count()
        })
        .from(userPermissionOverrides)
        .where(
          and(
            inArray(userPermissionOverrides.userId, userIds),
            eq(userPermissionOverrides.organizationId, organizationId),
            eq(userPermissionOverrides.isActive, true),
            isNull(userPermissionOverrides.revokedAt),
            or(isNull(userPermissionOverrides.expiresAt), gt(userPermissionOverrides.expiresAt, new Date()))
          )
        )
        .groupBy(userPermissionOverrides.userId)
    : [];

  const assignmentsByUser = new Map(
    assignmentRows.map((row) => [row.userId, { templateId: row.templateId, templateName: row.templateName }])
  );
  const overrideCountByUser = new Map(overrideCountRows.map((row) => [row.userId, row.value]));

  return {
    offset,
    limit,
    total: totalResult?.value ?? 0,
    userPermissions: rows.map((row) => ({
      user_id: row.userId,
      employee_code: row.employeeCode,
      full_name: row.fullName,
      email: row.email,
      department_name: row.departmentName,
      position_name: row.positionName,
      template_id: assignmentsByUser.get(row.userId)?.templateId ?? null,
      template_name: assignmentsByUser.get(row.userId)?.templateName ?? null,
      override_count: overrideCountByUser.get(row.userId) ?? 0,
      updated_at: row.updatedAt.toISOString()
    }))
  };
}

export async function getUserPermissionAssignmentDetailForOrganization({
  organizationId,
  userId
}: {
  organizationId: string;
  userId: string;
}): Promise<UserPermissionAssignmentDetail> {
  const [userRow] = await db
    .select({
      id: users.id,
      fullName: users.name,
      email: users.email,
      employeeCode: users.employeeCode,
      departmentName: departments.name,
      positionName: positions.name
    })
    .from(memberships)
    .innerJoin(users, eq(users.id, memberships.userId))
    .leftJoin(departments, eq(users.departmentId, departments.id))
    .leftJoin(positions, eq(users.positionId, positions.id))
    .where(and(eq(memberships.organizationId, organizationId), eq(users.id, userId)))
    .limit(1);

  if (!userRow) {
    throw new Error('NOT_FOUND');
  }

  const templates = await getTemplateOptions(organizationId);

  const assignmentRows = await db
    .select({
      id: userPermissionTemplateAssignments.id,
      templateId: userPermissionTemplateAssignments.templateId,
      templateName: permissionTemplates.name
    })
    .from(userPermissionTemplateAssignments)
    .innerJoin(permissionTemplates, eq(permissionTemplates.id, userPermissionTemplateAssignments.templateId))
    .where(
      and(
        eq(userPermissionTemplateAssignments.organizationId, organizationId),
        eq(userPermissionTemplateAssignments.userId, userId),
        eq(userPermissionTemplateAssignments.isActive, true)
      )
    );

  const overrideRows = await db
    .select({
      id: userPermissionOverrides.id,
      permission: userPermissionOverrides.permission,
      effect: userPermissionOverrides.effect,
      reason: userPermissionOverrides.reason,
      expiresAt: userPermissionOverrides.expiresAt,
      isActive: userPermissionOverrides.isActive,
      createdAt: userPermissionOverrides.createdAt
    })
    .from(userPermissionOverrides)
    .where(
      and(
        eq(userPermissionOverrides.organizationId, organizationId),
        eq(userPermissionOverrides.userId, userId),
        eq(userPermissionOverrides.isActive, true),
        isNull(userPermissionOverrides.revokedAt)
      )
    )
    .orderBy(desc(userPermissionOverrides.createdAt));

  const [membership] = await db
    .select({
      role: memberships.role,
      permissions: memberships.permissions
    })
    .from(memberships)
    .where(and(eq(memberships.organizationId, organizationId), eq(memberships.userId, userId)))
    .limit(1);

  const effective = await resolveEffectivePermissions({
    userId,
    organizationId,
    fallbackPermissions: membership?.permissions ?? [],
    fallbackRole: membership?.role ?? 'member'
  });

  return {
    user: {
      id: userRow.id,
      full_name: userRow.fullName,
      email: userRow.email,
      employee_code: userRow.employeeCode,
      department_name: userRow.departmentName,
      position_name: userRow.positionName
    },
    assignment: {
      template_id: assignmentRows[0]?.templateId ?? null,
      template_name: assignmentRows[0]?.templateName ?? null,
      permission_template_ids: assignmentRows.map((row) => row.templateId)
    },
    templates,
    overrides: overrideRows
      .filter((row) => normalizePermissions([row.permission]).length > 0)
      .map((row) => serializeOverride(row)),
    effective_permissions: effective.permissions,
    fallback_permissions: normalizePermissions(membership?.permissions ?? [])
  };
}

export async function saveUserPermissionAssignmentForOrganization({
  organizationId,
  actorUserId,
  userId,
  payload
}: {
  organizationId: string;
  actorUserId: string;
  userId: string;
  payload: UserPermissionAssignmentMutationPayload;
}) {
  const before = await getUserPermissionAssignmentDetailForOrganization({
    organizationId,
    userId
  });

  const now = new Date();
  const allowPermissions = normalizePermissions(payload.allow_permissions);
  const denyPermissions = normalizePermissions(payload.deny_permissions);

  // TODO: Schema currently lacks a DB-level unique constraint for one active template per user/org.
  // Enforce the single-active-assignment rule in application logic until a future migration tightens it.
  const assignmentResult = await db.transaction(async (tx) => {
    await tx
      .update(userPermissionTemplateAssignments)
      .set({
        isActive: false,
        updatedAt: now
      })
      .where(
        and(
          eq(userPermissionTemplateAssignments.organizationId, organizationId),
          eq(userPermissionTemplateAssignments.userId, userId),
          eq(userPermissionTemplateAssignments.isActive, true)
        )
      );

    const [existingAssignment] = await tx
      .select({
        id: userPermissionTemplateAssignments.id
      })
      .from(userPermissionTemplateAssignments)
      .where(
        and(
          eq(userPermissionTemplateAssignments.organizationId, organizationId),
          eq(userPermissionTemplateAssignments.userId, userId),
          eq(userPermissionTemplateAssignments.templateId, payload.template_id)
        )
      )
      .limit(1);

    let assignmentId = existingAssignment?.id ?? null;

    if (existingAssignment) {
      await tx
        .update(userPermissionTemplateAssignments)
        .set({
          isActive: true,
          assignedBy: actorUserId,
          assignedAt: now,
          updatedAt: now
        })
        .where(eq(userPermissionTemplateAssignments.id, existingAssignment.id));
    } else {
      const [createdAssignment] = await tx
        .insert(userPermissionTemplateAssignments)
        .values({
          organizationId,
          userId,
          templateId: payload.template_id,
          isActive: true,
          assignedBy: actorUserId,
          assignedAt: now,
          createdAt: now,
          updatedAt: now
        })
        .returning({ id: userPermissionTemplateAssignments.id });
      assignmentId = createdAssignment.id;
    }

    await tx
      .update(userPermissionOverrides)
      .set({
        isActive: false,
        revokedBy: actorUserId,
        revokedAt: now,
        updatedAt: now
      })
      .where(
        and(
          eq(userPermissionOverrides.organizationId, organizationId),
          eq(userPermissionOverrides.userId, userId),
          eq(userPermissionOverrides.isActive, true)
        )
      );

    if (allowPermissions.length > 0) {
      await tx.insert(userPermissionOverrides).values(
        allowPermissions.map((permission) => ({
          organizationId,
          userId,
          permission,
          effect: 'allow' as const,
          reason: payload.reason?.trim() || null,
          isActive: true,
          createdBy: actorUserId,
          createdAt: now,
          updatedAt: now
        }))
      );
    }

    if (denyPermissions.length > 0) {
      await tx.insert(userPermissionOverrides).values(
        denyPermissions.map((permission) => ({
          organizationId,
          userId,
          permission,
          effect: 'deny' as const,
          reason: payload.reason?.trim() || null,
          isActive: true,
          createdBy: actorUserId,
          createdAt: now,
          updatedAt: now
        }))
      );
    }

    return assignmentId;
  });

  const after = await getUserPermissionAssignmentDetailForOrganization({
    organizationId,
    userId
  });

  await logPermissionAuditEvent({
    organizationId,
    actorUserId,
    targetUserId: userId,
    templateId: payload.template_id,
    assignmentId: assignmentResult,
    action:
      before.assignment.template_id === null
        ? PERMISSION_AUDIT_ACTION.USER_TEMPLATE_ASSIGN
        : PERMISSION_AUDIT_ACTION.USER_TEMPLATE_CHANGE,
    reason: payload.reason ?? null,
    before: toPermissionAuditValue(before),
    after: toPermissionAuditValue(after)
  });
}

export async function createUserPermissionOverrideForOrganization({
  organizationId,
  actorUserId,
  userId,
  payload
}: {
  organizationId: string;
  actorUserId: string;
  userId: string;
  payload: UserPermissionOverrideMutationPayload;
}) {
  const now = new Date();
  const [created] = await db
    .insert(userPermissionOverrides)
    .values({
      organizationId,
      userId,
      permission: payload.permission,
      effect: payload.effect,
      reason: payload.reason?.trim() || null,
      expiresAt: payload.expires_at ? new Date(payload.expires_at) : null,
      isActive: true,
      createdBy: actorUserId,
      createdAt: now,
      updatedAt: now
    })
    .returning({ id: userPermissionOverrides.id });

  await logPermissionAuditEvent({
    organizationId,
    actorUserId,
    targetUserId: userId,
    overrideId: created.id,
    action:
      payload.effect === 'allow'
        ? PERMISSION_AUDIT_ACTION.USER_OVERRIDE_ALLOW
        : PERMISSION_AUDIT_ACTION.USER_OVERRIDE_DENY,
    reason: payload.reason ?? null,
    after: toPermissionAuditValue(payload)
  });

  return created.id;
}

export async function updateUserPermissionOverrideForOrganization({
  organizationId,
  actorUserId,
  userId,
  overrideId,
  payload
}: {
  organizationId: string;
  actorUserId: string;
  userId: string;
  overrideId: number;
  payload: UserPermissionOverrideMutationPayload;
}) {
  const [current] = await db
    .select({
      id: userPermissionOverrides.id,
      permission: userPermissionOverrides.permission,
      effect: userPermissionOverrides.effect,
      reason: userPermissionOverrides.reason,
      expiresAt: userPermissionOverrides.expiresAt
    })
    .from(userPermissionOverrides)
    .where(
      and(
        eq(userPermissionOverrides.id, overrideId),
        eq(userPermissionOverrides.organizationId, organizationId),
        eq(userPermissionOverrides.userId, userId)
      )
    )
    .limit(1);

  if (!current) {
    throw new Error('NOT_FOUND');
  }

  await db
    .update(userPermissionOverrides)
    .set({
      permission: payload.permission,
      effect: payload.effect,
      reason: payload.reason?.trim() || null,
      expiresAt: payload.expires_at ? new Date(payload.expires_at) : null,
      updatedAt: new Date()
    })
    .where(eq(userPermissionOverrides.id, overrideId));

  await logPermissionAuditEvent({
    organizationId,
    actorUserId,
    targetUserId: userId,
    overrideId,
    action: PERMISSION_AUDIT_ACTION.USER_OVERRIDE_UPDATE,
    reason: payload.reason ?? null,
    before: toPermissionAuditValue(current),
    after: toPermissionAuditValue(payload)
  });
}

export async function deleteUserPermissionOverrideForOrganization({
  organizationId,
  actorUserId,
  userId,
  overrideId
}: {
  organizationId: string;
  actorUserId: string;
  userId: string;
  overrideId: number;
}) {
  const [current] = await db
    .select({
      id: userPermissionOverrides.id,
      permission: userPermissionOverrides.permission,
      effect: userPermissionOverrides.effect,
      reason: userPermissionOverrides.reason
    })
    .from(userPermissionOverrides)
    .where(
      and(
        eq(userPermissionOverrides.id, overrideId),
        eq(userPermissionOverrides.organizationId, organizationId),
        eq(userPermissionOverrides.userId, userId)
      )
    )
    .limit(1);

  if (!current) {
    throw new Error('NOT_FOUND');
  }

  await db
    .update(userPermissionOverrides)
    .set({
      isActive: false,
      revokedBy: actorUserId,
      revokedAt: new Date(),
      updatedAt: new Date()
    })
    .where(eq(userPermissionOverrides.id, overrideId));

  await logPermissionAuditEvent({
    organizationId,
    actorUserId,
    targetUserId: userId,
    overrideId,
    action: PERMISSION_AUDIT_ACTION.USER_OVERRIDE_REMOVE,
    before: toPermissionAuditValue(current)
  });
}
