import bcrypt from 'bcryptjs';
import { Prisma } from '@prisma/client';
import { prisma } from '../../config/database';
import { AppError } from '../../middleware/error-handler';
import { getPrimaryRole } from '../../utils/roles';
import { userProvisioningService } from '../../services/user-provisioning/provisioning.service';
import { eventBus } from '../../services/event-bus.service';
import { logger } from '../../utils/logger';
import type { CMUser } from '../../services/user-provisioning/provisioner.interface';
import type { CreateUserInput, UpdateUserInput, ListUsersInput } from './users.schemas';

const userSelect = {
  id: true,
  email: true,
  name: true,
  phone: true,
  pronouns: true,
  role: true,
  roles: true,
  status: true,
  permissions: true,
  createdVia: true,
  expiresAt: true,
  expireDays: true,
  lastLoginAt: true,
  emailVerified: true,
  createdAt: true,
  updatedAt: true,
} satisfies Prisma.UserSelect;

export const usersService = {
  async findAll(filters: ListUsersInput) {
    const { page, limit, search, role, status } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.UserWhereInput = {};

    if (search) {
      where.OR = [
        { email: { contains: search, mode: 'insensitive' } },
        { name: { contains: search, mode: 'insensitive' } },
      ];
    }

    if (role) where.role = role;
    if (status) where.status = status;

    const [users, total] = await Promise.all([
      prisma.user.findMany({
        where,
        select: userSelect,
        skip,
        take: limit,
        orderBy: { createdAt: 'desc' },
      }),
      prisma.user.count({ where }),
    ]);

    return {
      users,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  },

  async findById(id: string) {
    const user = await prisma.user.findUnique({
      where: { id },
      select: userSelect,
    });

    if (!user) {
      throw new AppError(404, 'User not found', 'USER_NOT_FOUND');
    }

    return user;
  },

  async create(data: CreateUserInput) {
    const existing = await prisma.user.findUnique({ where: { email: data.email } });
    if (existing) {
      throw new AppError(409, 'Email already registered', 'EMAIL_EXISTS');
    }

    const hashedPassword = await bcrypt.hash(data.password, 12);

    // Compute roles array and primary role
    const roles = data.roles && data.roles.length > 0
      ? data.roles
      : [data.role || 'USER'];
    const primaryRole = getPrimaryRole(roles as any);

    const user = await prisma.user.create({
      data: {
        ...data,
        password: hashedPassword,
        role: primaryRole,
        roles: JSON.parse(JSON.stringify(roles)),
        emailVerified: true, // Admin-created users are pre-verified
        expiresAt: data.expiresAt ? new Date(data.expiresAt) : undefined,
      },
      select: userSelect,
    });

    // Fire-and-forget: auto-link to existing Contact with matching email
    prisma.contact.findFirst({
      where: { email: { equals: data.email, mode: 'insensitive' }, userId: null, mergedIntoId: null },
    }).then(async (existingContact) => {
      if (existingContact) {
        await prisma.contact.update({ where: { id: existingContact.id }, data: { userId: user.id } });
        logger.info(`Auto-linked contact ${existingContact.id} to new user ${user.id}`);
      }
    }).catch(err => {
      logger.warn('Auto-link contact on user creation failed:', err);
    });

    // Fire-and-forget: provision to eager services
    userProvisioningService.onUserCreated(toCMUser(user)).catch(err => {
      logger.warn('User provisioning hook (create) failed:', err);
    });

    eventBus.publish('user.created', {
      userId: user.id,
      email: user.email,
      name: user.name || '',
      role: user.role,
    });

    return user;
  },

  async update(id: string, data: UpdateUserInput) {
    const existing = await prisma.user.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'User not found', 'USER_NOT_FOUND');
    }

    if (data.email && data.email !== existing.email) {
      const emailTaken = await prisma.user.findUnique({ where: { email: data.email } });
      if (emailTaken) {
        throw new AppError(409, 'Email already in use', 'EMAIL_EXISTS');
      }
    }

    const updateData: Prisma.UserUpdateInput = { ...data };

    if (data.password) {
      updateData.password = await bcrypt.hash(data.password, 12);
    }

    if (data.expiresAt !== undefined) {
      updateData.expiresAt = data.expiresAt ? new Date(data.expiresAt) : null;
    }

    // Sync roles and primary role
    if (data.roles) {
      updateData.roles = JSON.parse(JSON.stringify(data.roles));
      updateData.role = getPrimaryRole(data.roles as any);
    } else if (data.role) {
      // If only primary role changed, update roles array too
      const currentRoles = Array.isArray(existing.roles) ? existing.roles as string[] : [existing.role];
      if (!currentRoles.includes(data.role)) {
        updateData.roles = JSON.parse(JSON.stringify([...currentRoles, data.role]));
      }
    }

    const user = await prisma.user.update({
      where: { id },
      data: updateData,
      select: userSelect,
    });

    // Invalidate sessions when user is deactivated or banned
    const deactivatedStatuses = ['INACTIVE', 'BANNED', 'PENDING_APPROVAL', 'PENDING_VERIFICATION'];
    if (data.status && deactivatedStatuses.includes(data.status)) {
      await prisma.refreshToken.deleteMany({ where: { userId: id } });
    }

    // Invalidate sessions when password is changed by admin
    if (data.password) {
      await prisma.refreshToken.deleteMany({ where: { userId: id } });
    }

    // Fire-and-forget: sync changes to provisioned services
    userProvisioningService.onUserUpdated(toCMUser(user), data).catch(err => {
      logger.warn('User provisioning hook (update) failed:', err);
    });

    // Compute list of changed fields for the event payload
    const changes = Object.keys(data).filter(k => k !== 'currentPassword');
    eventBus.publish('user.updated', {
      userId: user.id,
      email: user.email,
      name: user.name || '',
      role: user.role,
      changes,
    });

    return user;
  },

  async delete(id: string) {
    const existing = await prisma.user.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'User not found', 'USER_NOT_FOUND');
    }

    // Fire-and-forget: deactivate in provisioned services BEFORE deleting
    const cmUser = toCMUser(existing as any);
    userProvisioningService.onUserDeactivated(cmUser).catch(err => {
      logger.warn('User provisioning hook (delete) failed:', err);
    });

    await prisma.user.delete({ where: { id } });

    eventBus.publish('user.deleted', {
      userId: existing.id,
      email: existing.email,
      name: existing.name || '',
    });
  },
};

/** Convert a Prisma User (or select result) to the CMUser shape needed by provisioners */
function toCMUser(user: {
  id: string; email: string; name: string | null; role: string;
  roles: unknown; status: string; permissions: unknown;
}): CMUser {
  return {
    id: user.id,
    email: user.email,
    name: user.name,
    role: user.role,
    roles: user.roles,
    status: user.status,
    permissions: user.permissions as Record<string, unknown> | null,
  };
}
