import {
  ConflictException,
  BadRequestException,
  ForbiddenException,
  HttpException,
  HttpStatus,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { createHash, randomBytes } from 'node:crypto';
import { ConfigService } from '@nestjs/config';
import { ServiceLogger } from '../../common/logging/service-logger';

import { ActivityLogService } from '../activity-logs/activity-log.service';
import { EmailService } from '../email/email.service';
import { PrismaService } from '../../common/prisma/prisma.service';
import type {
  ActivityAction,
  ActivityEntityType,
  Prisma,
} from '../../generated/prisma/client';
import type { UserRole, UserStatus } from '../../generated/prisma/enums';
import { CreateUserDto } from './dto/create-user.dto';
import { ListUsersQueryDto } from './dto/list-users-query.dto';
import { UpdateUserDto } from './dto/update-user.dto';

export type AdminUserRecord = {
  id: string;
  firstName: string;
  lastName: string;
  name: string;
  email: string;
  role: UserRole;
  status: UserStatus;
  isDeleted: boolean;
  createdAt: Date;
  updatedAt: Date;
  passwordSetupPending: boolean;
};

export type AdminUsersSummary = {
  total: number;
  superAdmins: number;
  managers: number;
  active: number;
};

export type AdminUsersListResponse = {
  items: AdminUserRecord[];
  meta: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
  summary: AdminUsersSummary;
};

@Injectable()
export class UsersService {
  private readonly logger = new ServiceLogger(UsersService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly activityLogService: ActivityLogService,
    private readonly emailService: EmailService,
    private readonly configService: ConfigService,
  ) {}

  private get userClient() {
    return this.prisma.user as any;
  }

  async listUsers(query: ListUsersQueryDto): Promise<AdminUsersListResponse> {
    const page = query.page ?? 1;
    const limit = query.limit ?? 5;
    const where = this.buildUserWhere(query);

    const [total, summary] = await Promise.all([
      this.userClient.count({ where }),
      this.getUsersSummary(),
    ]);

    const totalPages = Math.max(1, Math.ceil(total / limit));
    const safePage = Math.min(page, totalPages);

    const items = await this.userClient.findMany({
      where,
      orderBy: {
        createdAt: 'desc',
      },
      skip: (safePage - 1) * limit,
      take: limit,
      select: this.userSelectWithPassword,
    });

    return {
      items: (items as UserRecordWithPassword[]).map((user) =>
        this.serializeUser(user),
      ),
      meta: {
        page: safePage,
        limit,
        total,
        totalPages,
      },
      summary,
    };
  }

  listAssignableUsers(): Promise<AdminUserRecord[]> {
    return this.userClient.findMany({
      where: {
        role: { in: ['super_admin', 'seo_manager'] },
        status: 'active',
        isDeleted: false,
      },
      orderBy: {
        createdAt: 'desc',
      },
      select: this.userSelectWithPassword,
    }).then((users: UserRecordWithPassword[]) =>
      users.map((user) => this.serializeUser(user)),
    );
  }

  listSeoManagers(): Promise<AdminUserRecord[]> {
    return this.userClient.findMany({
      where: {
        role: 'seo_manager',
        isDeleted: false,
      },
      orderBy: {
        createdAt: 'desc',
      },
      select: this.userSelectWithPassword,
    }).then((users: UserRecordWithPassword[]) =>
      users.map((user) => this.serializeUser(user)),
    );
  }

  async createUser(
    dto: CreateUserDto,
    actorUserId?: string | null,
  ): Promise<AdminUserRecord> {
    const email = dto.email.trim().toLowerCase();
    const firstName = dto.firstName.trim();
    const lastName = dto.lastName.trim();
    const name = this.buildDisplayName(firstName, lastName);
    const passwordHash = await bcrypt.hash(dto.password.trim(), 10);

    const existing = await this.userClient.findUnique({
      where: { email },
      select: { id: true },
    });

    if (existing) {
      throw new ConflictException('A user with this email already exists.');
    }

    const user = await this.userClient.create({
      data: {
        firstName,
        lastName,
        name,
        email,
        password: passwordHash,
        role: dto.role,
        status: dto.status,
        isDeleted: false,
      },
      select: this.userSelectWithPassword,
    });

    await this.recordActivity({
      action: 'user_created',
      entityType: 'user',
      entityId: user.id,
      title: 'User created',
      description: `${name} was created as a ${this.roleLabel(dto.role)}.`,
      actorUserId,
      metadata: {
        email,
        role: dto.role,
        status: dto.status,
      },
    });

    // Legacy invitation flow retained for future reuse. Direct user creation now
    // stores the administrator-provided password and does not send setup email.
    // await this.issuePasswordSetupEmail(user);
    this.logger.log('user.created', { userId: user.id, actorUserId, role: user.role });

    return this.serializeUser(user);
  }

  async updateUser(
    userId: string,
    dto: UpdateUserDto,
    actorUserId?: string | null,
  ): Promise<AdminUserRecord> {
    const current = await this.userClient.findFirst({
      where: {
        id: userId,
        isDeleted: false,
      },
      select: { ...this.userSelect, password: true },
    });

    if (!current) {
      throw new NotFoundException('User not found.');
    }

    const firstName = dto.firstName?.trim() ?? current.firstName;
    const lastName = dto.lastName?.trim() ?? current.lastName;
    const name = this.buildDisplayName(firstName, lastName);

    const hasPasswordUpdate =
      typeof dto.password === 'string' && dto.password.trim().length > 0;

    if (hasPasswordUpdate && !current.password) {
      throw new ConflictException(
        'This user must set their password through the account setup email.',
      );
    }

    const password = hasPasswordUpdate
      ? await bcrypt.hash(dto.password!.trim(), 10)
      : undefined;

    const updated = await this.userClient.update({
      where: { id: userId },
      data: {
        firstName,
        lastName,
        name,
        password,
        role: dto.role,
        // A pending invitation cannot be activated before its password is set.
        status: current.password ? dto.status : 'inactive',
      },
      select: this.userSelectWithPassword,
    });

    await this.recordActivity({
      action: 'user_updated',
      entityType: 'user',
      entityId: updated.id,
      title: 'User updated',
      description: this.describeUserUpdate(current, updated, hasPasswordUpdate),
      actorUserId,
      metadata: {
        email: updated.email,
        role: updated.role,
        status: updated.status,
      },
    });
    this.logger.log('user.updated', { userId: updated.id, actorUserId });

    return this.serializeUser(updated);
  }

  async softDeleteUser(userId: string, actorUserId?: string | null) {
    if (actorUserId && userId === actorUserId) {
      throw new ForbiddenException('You cannot delete your own account.');
    }

    const current = await this.userClient.findFirst({
      where: {
        id: userId,
        isDeleted: false,
      },
      select: { ...this.userSelect, password: true },
    });

    if (!current) {
      throw new NotFoundException('User not found.');
    }

    const assignedClientCount = await this.prisma.client.count({
      where: {
        assignedUserId: current.id,
        isDeleted: false,
      },
    });

    if (assignedClientCount > 0) {
      throw new ConflictException(
        `${current.name} is assigned to ${assignedClientCount} client${assignedClientCount === 1 ? '' : 's'}. Reassign ${assignedClientCount === 1 ? 'this client' : 'these clients'} before deleting this user.`,
      );
    }

    const deleted = await this.userClient.update({
      where: { id: userId },
      data: {
        isDeleted: true,
        status: 'inactive',
        refreshTokenHash: null,
      },
      select: this.userSelectWithPassword,
    });

    await this.recordActivity({
      action: 'user_status_changed',
      entityType: 'user',
      entityId: deleted.id,
      title: 'User deleted',
      description: `${deleted.name} was marked as deleted.`,
      actorUserId,
      metadata: {
        email: deleted.email,
        role: deleted.role,
      },
    });
    this.logger.log('user.deleted', { userId: deleted.id, actorUserId });

    return this.serializeUser(deleted);
  }

  async restoreUser(userId: string, actorUserId?: string | null) {
    const current = await this.userClient.findFirst({
      where: {
        id: userId,
        isDeleted: true,
      },
      select: { ...this.userSelect, password: true },
    });

    if (!current) {
      throw new NotFoundException('User not found.');
    }

    const restored = await this.userClient.update({
      where: { id: userId },
      data: {
        isDeleted: false,
        status: current.password ? 'active' : 'inactive',
      },
      select: this.userSelectWithPassword,
    });

    await this.recordActivity({
      action: 'user_status_changed',
      entityType: 'user',
      entityId: restored.id,
      title: 'User restored',
      description: `${restored.name} was restored${restored.status === 'active' ? ' and reactivated' : ' and remains inactive'}.`,
      actorUserId,
      metadata: {
        email: restored.email,
        role: restored.role,
      },
    });
    this.logger.log('user.restored', { userId: restored.id, actorUserId });

    return this.serializeUser(restored);
  }

  async resendPasswordSetupEmail(userId: string, actorUserId?: string | null) {
    const user = await this.userClient.findFirst({
      where: { id: userId, isDeleted: false },
      select: { ...this.userSelect, password: true },
    });

    if (!user) throw new NotFoundException('User not found.');
    if (user.password) {
      throw new ConflictException('This user has already set up their password.');
    }

    await this.assertPasswordSetupResendAllowed(user.id);
    await this.issuePasswordSetupEmail(user);
    await this.recordActivity({
      action: 'user_updated', entityType: 'user', entityId: user.id,
      title: 'Password setup email resent',
      description: `A password setup email was resent to ${user.name}.`,
      actorUserId,
    });
    this.logger.log('user.password_setup.resent', { userId: user.id, actorUserId });
    return { success: true };
  }

  async validatePasswordSetupToken(token: string) {
    const setupToken = await this.getValidPasswordSetupToken(token);
    return { valid: true, name: setupToken.user.name, email: setupToken.user.email };
  }

  async completePasswordSetup(token: string, password: string) {
    const tokenHash = this.hashSetupToken(token);
    const now = new Date();
    const normalizedPassword = password.trim();
    const passwordHash = await bcrypt.hash(normalizedPassword, 10);

    const activated = await this.prisma.$transaction(async (tx) => {
      const setupToken = await tx.passwordSetupToken.findFirst({
        where: { tokenHash, usedAt: null, expiresAt: { gt: now }, user: { isDeleted: false } },
        include: { user: true },
      });
      if (!setupToken) throw new BadRequestException('This password setup link is invalid or has expired.');

      const consumed = await tx.passwordSetupToken.updateMany({
        where: { id: setupToken.id, usedAt: null, expiresAt: { gt: now } },
        data: { usedAt: now },
      });
      if (consumed.count !== 1) throw new BadRequestException('This password setup link has already been used.');

      const user = await tx.user.update({
        where: { id: setupToken.userId },
        data: { password: passwordHash, status: 'active', refreshTokenHash: null },
      });
      await tx.passwordSetupToken.updateMany({
        where: { userId: user.id, usedAt: null }, data: { usedAt: now },
      });
      return user;
    });

    await this.recordActivity({
      action: 'user_status_changed', entityType: 'user', entityId: activated.id,
      title: 'Account activated', description: `${activated.name} set a password and activated their account.`,
      actorUserId: activated.id,
    });
    this.logger.log('user.password_setup.completed', { userId: activated.id });
    return { success: true };
  }

  findByEmail(email: string) {
    return this.userClient.findFirst({
      where: {
        email: email.trim().toLowerCase(),
        isDeleted: false,
      },
    });
  }

  findById(id: string) {
    return this.userClient.findFirst({
      where: {
        id,
        isDeleted: false,
      },
    });
  }

  updateRefreshTokenHash(userId: string, refreshTokenHash: string | null) {
    return this.userClient.update({
      where: { id: userId },
      data: {
        refreshTokenHash,
      },
    });
  }

  private async issuePasswordSetupEmail(user: Pick<AdminUserRecord, 'id' | 'email' | 'name'>) {
    try {
      const now = new Date();
      const expiresInHours = this.getPasswordSetupExpiryHours();
      const rawToken = randomBytes(32).toString('hex');
      await this.prisma.passwordSetupToken.updateMany({
        where: { userId: user.id, usedAt: null }, data: { usedAt: now },
      });
      await this.prisma.passwordSetupToken.create({
        data: { userId: user.id, tokenHash: this.hashSetupToken(rawToken), expiresAt: new Date(now.getTime() + expiresInHours * 3_600_000) },
      });
      const frontendUrl = this.configService.getOrThrow<string>('FRONTEND_URL').replace(/\/+$/, '');
      const result = await this.emailService.sendPasswordSetupEmail({
        recipient: { email: user.email, name: user.name },
        setupUrl: `${frontendUrl}/set-password?token=${encodeURIComponent(rawToken)}`,
        expiresInHours,
      });

      if (result.status === 'failed') {
        this.logger.warn('user.password_setup.email_failed', { userId: user.id });
      } else {
        this.logger.log('user.password_setup.email_processed', {
          userId: user.id,
          emailStatus: result.status,
        });
      }
    } catch (error) {
      // Email delivery must never undo or block a successfully created account.
      this.logger.error('user.password_setup.email_preparation_failed', error, {
        userId: user.id,
      });
    }
  }

  private async assertPasswordSetupResendAllowed(userId: string) {
    const now = new Date();
    const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
    const [latestToken, tokenCountLastDay] = await Promise.all([
      this.prisma.passwordSetupToken.findFirst({
        where: { userId },
        orderBy: { createdAt: 'desc' },
        select: { createdAt: true },
      }),
      this.prisma.passwordSetupToken.count({
        where: { userId, createdAt: { gte: oneDayAgo } },
      }),
    ]);

    if (latestToken) {
      const elapsedMs = now.getTime() - latestToken.createdAt.getTime();
      const cooldownMs = 60 * 1000;
      if (elapsedMs < cooldownMs) {
        const remainingSeconds = Math.ceil((cooldownMs - elapsedMs) / 1000);
        this.logger.warn('user.password_setup.resend_rejected', {
          userId, reason: 'cooldown_active', remainingSeconds,
        });
        throw new HttpException(
          `Please wait ${remainingSeconds} seconds before resending the password setup email.`,
          HttpStatus.TOO_MANY_REQUESTS,
        );
      }
    }

    if (tokenCountLastDay >= 5) {
      this.logger.warn('user.password_setup.resend_rejected', {
        userId, reason: 'daily_limit_reached', tokenCountLastDay,
      });
      throw new HttpException(
        'This user has reached the limit of five password setup emails in 24 hours.',
        HttpStatus.TOO_MANY_REQUESTS,
      );
    }
  }

  private async getValidPasswordSetupToken(token: string) {
    const setupToken = await this.prisma.passwordSetupToken.findFirst({
      where: { tokenHash: this.hashSetupToken(token), usedAt: null, expiresAt: { gt: new Date() }, user: { isDeleted: false } },
      include: { user: { select: { name: true, email: true } } },
    });
    if (!setupToken) throw new BadRequestException('This password setup link is invalid or has expired.');
    return setupToken;
  }

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

  private getPasswordSetupExpiryHours() {
    const configured = Number(this.configService.get<string>('PASSWORD_SETUP_TOKEN_TTL_HOURS') ?? 24);
    return Number.isFinite(configured) && configured >= 1 && configured <= 168 ? configured : 24;
  }

  private buildDisplayName(firstName: string, lastName: string) {
    return `${firstName} ${lastName}`.trim().replace(/\s+/g, ' ');
  }

  private buildUserWhere(query: ListUsersQueryDto): Prisma.UserWhereInput {
    const search = query.search?.trim();
    return {
      ...(query.role ? { role: query.role } : {}),
      ...(query.status === 'deleted'
        ? { isDeleted: true }
        : query.status
          ? { status: query.status, isDeleted: false }
          : {}),
      ...(search
        ? {
            OR: [
              { firstName: { contains: search, mode: 'insensitive' } },
              { lastName: { contains: search, mode: 'insensitive' } },
              { email: { contains: search, mode: 'insensitive' } },
              { name: { contains: search, mode: 'insensitive' } },
            ],
          }
        : {}),
    };
  }

  private async getUsersSummary(): Promise<AdminUsersSummary> {
    const baseWhere: Prisma.UserWhereInput = {
      isDeleted: false,
    };

    const [total, superAdmins, managers, active] = await Promise.all([
      this.userClient.count({ where: baseWhere }),
      this.userClient.count({
        where: {
          ...baseWhere,
          role: 'super_admin',
        },
      }),
      this.userClient.count({
        where: {
          ...baseWhere,
          role: 'seo_manager',
        },
      }),
      this.userClient.count({
        where: {
          ...baseWhere,
          status: 'active',
        },
      }),
    ]);

    return { total, superAdmins, managers, active };
  }

  private roleLabel(role: UserRole) {
    return role === 'super_admin' ? 'Super Admin' : 'SEO Manager';
  }

  private describeUserUpdate(
    current: AdminUserRecord,
    updated: AdminUserRecord,
    passwordChanged: boolean,
  ) {
    const changes = [
      this.describeChange('Name', current.name, updated.name),
      this.describeChange(
        'Role',
        this.roleLabel(current.role),
        this.roleLabel(updated.role),
      ),
      this.describeChange(
        'Status',
        this.capitalize(current.status),
        this.capitalize(updated.status),
      ),
    ].filter((change): change is string => Boolean(change));

    if (passwordChanged) {
      changes.push('Password was changed');
    }

    return changes.length > 0
      ? `${updated.name}: ${changes.join('; ')}.`
      : `${updated.name} was updated with no visible field changes.`;
  }

  private describeChange(label: string, before: string, after: string) {
    return before === after
      ? null
      : `${label} changed from "${before}" to "${after}"`;
  }

  private capitalize(value: string) {
    return value.charAt(0).toUpperCase() + value.slice(1);
  }

  private async recordActivity(input: {
    action: ActivityAction;
    entityType: ActivityEntityType;
    entityId?: string | null;
    title: string;
    description: string;
    metadata?: Prisma.InputJsonValue | null;
    actorUserId?: string | null;
  }) {
    try {
      await this.activityLogService.record({
        action: input.action,
        entityType: input.entityType,
        entityId: input.entityId,
        title: input.title,
        description: input.description,
        metadata: input.metadata,
        actorUserId: input.actorUserId,
      });
    } catch (error) {
      // Activity logging must never block user management.
      this.logger.error('user.activity_log.failed', error, {
        action: input.action, entityId: input.entityId, actorUserId: input.actorUserId,
      });
    }
  }

  private readonly userSelect = {
    id: true,
    firstName: true,
    lastName: true,
    name: true,
    email: true,
    role: true,
    status: true,
    isDeleted: true,
    createdAt: true,
    updatedAt: true,
  } as const;

  private readonly userSelectWithPassword = {
    ...this.userSelect,
    password: true,
  } as const;

  private serializeUser(user: UserRecordWithPassword): AdminUserRecord {
    const { password, ...safeUser } = user;
    return {
      ...safeUser,
      passwordSetupPending: password === null,
    };
  }
}

type UserRecordWithPassword = Omit<AdminUserRecord, 'passwordSetupPending'> & {
  password: string | null;
};
