import {
  BadRequestException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { createHash, randomBytes } from 'node:crypto';
import { AuthenticatedUser } from '../auth/auth.types';
import { normalizeEmail, normalizeText } from '../common/normalize';
import { hasMinimumPasswordLength, hashPassword } from '../common/password';
import { getAppConfig } from '../config/env';
import { MailerService } from '../mailer/mailer.service';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { CompleteInvitationDto } from './dto/complete-invitation.dto';

const config = getAppConfig();

@Injectable()
export class UsersService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly mailer: MailerService,
  ) {}

  async findAll(params?: {
    page?: number;
    limit?: number;
    search?: string;
    roleId?: string;
    status?: string;
  }) {
    const page = Math.max(1, params?.page || 1);
    const limit = Math.min(50, Math.max(1, params?.limit || 10));
    const where = this.buildWhere(params);

    const [users, total] = await Promise.all([
      this.prisma.user.findMany({
        where,
        include: {
          role: {
            select: { id: true, name: true },
          },
        },
        orderBy: { createdAt: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.user.count({ where }),
    ]);

    return {
      items: users.map((user) => ({
        id: user.id,
        name: user.name,
        email: user.email,
        status: user.status,
        roleId: user.roleId,
        roleName: user.role?.name || 'Unassigned',
        createdAt: user.createdAt,
        updatedAt: user.updatedAt,
        lastLoginAt: user.lastLoginAt,
        emailVerifiedAt: user.emailVerifiedAt,
      })),
      total,
      page,
      pageSize: limit,
      totalPages: Math.max(1, Math.ceil(total / limit)),
    };
  }

  async exportCsv(params?: {
    search?: string;
    roleId?: string;
    status?: string;
  }) {
    const users = await this.prisma.user.findMany({
      where: this.buildWhere(params),
      include: {
        role: {
          select: { id: true, name: true },
        },
      },
      orderBy: { createdAt: 'desc' },
    });

    const header = [
      'Name',
      'Email',
      'Role',
      'Status',
      'Created At',
      'Updated At',
      'Last Login At',
      'Email Verified At',
    ];
    const rows = users.map((user) =>
      [
        user.name,
        user.email,
        user.role?.name || 'Unassigned',
        user.status,
        user.createdAt.toISOString(),
        user.updatedAt.toISOString(),
        user.lastLoginAt?.toISOString() || '',
        user.emailVerifiedAt?.toISOString() || '',
      ]
        .map((cell) => `"${String(cell).replaceAll('"', '""')}"`)
        .join(','),
    );

    return [header.join(','), ...rows].join('\n');
  }

  async invite(dto: CreateUserDto, actorUser: AuthenticatedUser, ipAddress?: string) {
    const actor = await this.findActor(actorUser.sub, actorUser.email);
    const email = normalizeEmail(dto.email);
    const name = normalizeText(dto.name);
    if (!name) {
      throw new BadRequestException('Name is required');
    }

    const role = dto.roleId
      ? await this.prisma.role.findUnique({ where: { id: dto.roleId } })
      : await this.prisma.role.findUnique({ where: { key: 'agent' } });

    if (dto.roleId && !role) {
      throw new NotFoundException('Role not found');
    }

    const existingUser = await this.prisma.user.findUnique({
      where: { email },
      select: { id: true, status: true },
    });

    if (existingUser?.status === 'active') {
      throw new BadRequestException('User already exists and is active');
    }

    const token = randomBytes(24).toString('hex');
    const tokenHash = this.hashToken(token);
    const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);

    const user = await this.prisma.user.upsert({
      where: { email },
      update: {
        name,
        roleId: role?.id || null,
        status: 'invited',
        inviteTokenHash: tokenHash,
        inviteTokenExpiresAt: expiresAt,
        passwordHash: null,
        emailVerifiedAt: null,
      },
      create: {
        name,
        email,
        roleId: role?.id || null,
        status: 'invited',
        inviteTokenHash: tokenHash,
        inviteTokenExpiresAt: expiresAt,
      },
      include: {
        role: {
          select: { name: true },
        },
      },
    });

    const invitationUrl = `${config.frontendOrigin}/invite/${token}`;
    let mailResult: { delivered: boolean } = { delivered: false };
    try {
      mailResult = await this.mailer.sendInvitationEmail({
        to: user.email,
        name: user.name,
        roleName: user.role?.name || 'User',
        invitationUrl,
        invitedBy: actor.name,
      });
    } catch {
      mailResult = { delivered: false };
    }

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actionType: 'User Invited',
        module: 'User Management',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Invited user ${user.email} as ${user.role?.name || 'Unassigned'}.`,
      },
    });

    return {
      id: user.id,
      name: user.name,
      email: user.email,
      status: user.status,
      roleId: user.roleId,
      roleName: user.role?.name || 'Unassigned',
      invitationUrl,
      emailSent: mailResult.delivered,
    };
  }

  async update(id: string, dto: UpdateUserDto, actorUser: AuthenticatedUser, ipAddress?: string) {
    const actor = await this.findActor(actorUser.sub, actorUser.email);
    if (dto.roleId) {
      const role = await this.prisma.role.findUnique({ where: { id: dto.roleId } });
      if (!role) {
        throw new NotFoundException('Role not found');
      }
    }

    const user = await this.prisma.user.update({
      where: { id },
      data: {
        ...(dto.name !== undefined ? { name: normalizeText(dto.name) || '' } : {}),
        ...(dto.email !== undefined ? { email: normalizeEmail(dto.email) } : {}),
        ...(dto.roleId !== undefined ? { roleId: dto.roleId || null } : {}),
        ...(dto.status !== undefined ? { status: dto.status } : {}),
      },
      include: {
        role: {
          select: { name: true },
        },
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actionType: 'User Updated',
        module: 'User Management',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Updated user ${user.email}.`,
      },
    });

    return {
      id: user.id,
      name: user.name,
      email: user.email,
      status: user.status,
      roleId: user.roleId,
      roleName: user.role?.name || 'Unassigned',
      lastLoginAt: user.lastLoginAt,
      emailVerifiedAt: user.emailVerifiedAt,
    };
  }

  async getInvitation(token: string) {
    const invite = await this.findInvitationByToken(token);
    return {
      email: invite.email,
      name: invite.name,
      roleName: invite.role?.name || 'User',
      expiresAt: invite.inviteTokenExpiresAt,
    };
  }

  async completeInvitation(token: string, dto: CompleteInvitationDto, ipAddress?: string) {
    if (!hasMinimumPasswordLength(dto.password)) {
      throw new BadRequestException('Password must be at least 8 characters');
    }

    const invite = await this.findInvitationByToken(token);
    const passwordHash = await hashPassword(dto.password);

    const user = await this.prisma.user.update({
      where: { id: invite.id },
      data: {
        passwordHash,
        status: 'active',
        emailVerifiedAt: new Date(),
        inviteTokenHash: null,
        inviteTokenExpiresAt: null,
      },
      include: {
        role: {
          select: { name: true },
        },
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'User Activated',
        module: 'User Management',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Completed password setup for ${user.email}.`,
      },
    });

    return {
      id: user.id,
      email: user.email,
      status: user.status,
    };
  }

  private async findInvitationByToken(token: string) {
    const invite = await this.prisma.user.findFirst({
      where: {
        inviteTokenHash: this.hashToken(token),
      },
      include: {
        role: {
          select: { name: true },
        },
      },
    });

    if (!invite || !invite.inviteTokenExpiresAt || invite.inviteTokenExpiresAt.getTime() < Date.now()) {
      throw new UnauthorizedException('Invitation token is invalid or expired');
    }

    return invite;
  }

  private async findActor(userId: string, email: string) {
    const actor = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, name: true, email: true },
    });

    return {
      id: actor?.id || userId,
      name: actor?.name || email,
      email: actor?.email || email,
    };
  }

  private hashToken(token: string) {
    return createHash('sha256').update(token).digest('hex');
  }

  private buildWhere(params?: {
    search?: string;
    roleId?: string;
    status?: string;
  }): Prisma.UserWhereInput {
    const search = params?.search?.trim();
    const roleId = params?.roleId?.trim();
    const status = params?.status?.trim();

    const and: Prisma.UserWhereInput[] = [];

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

    if (roleId) {
      and.push({ roleId });
    }

    if (status && ['invited', 'active', 'inactive', 'suspended'].includes(status)) {
      and.push({ status: status as 'invited' | 'active' | 'inactive' | 'suspended' });
    }

    return and.length > 0 ? { AND: and } : {};
  }
}
