import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import * as argon from '@node-rs/argon2';
import { Prisma } from 'generated/prisma/client';
import { BootstrapTableRequestDto } from 'src/commons/dto/bootstrap-table-request.dto';
import { BootstrapTableResponseDto } from 'src/commons/dto/bootstrap-table-response.dto';
import { buildWhereClause } from 'src/commons/utils/prisma-query-builder';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) { }

  /**
   * Create a new user.  Password is hashed and branch relations are handled
   * if `branchIds` is provided.
   */
  async create(dto: CreateUserDto) {
    const hash = await argon.hash(dto.password);

    try {
      const user = await this.prisma.user.create({
        data: {
          email: dto.email,
          hash,
          firstName: dto.firstName,
          lastName: dto.lastName,
          roleId: dto.roleId,
        },
      });

      if (dto.branchIds && dto.branchIds.length) {
        // attach to branches using upsert to avoid duplicates
        const ops = dto.branchIds.map((branchId) =>
          this.prisma.userBranch.upsert({
            where: {
              userId_branchId: {
                userId: user.id,
                branchId,
              },
            },
            update: {},
            create: {
              userId: user.id,
              branchId,
            },
          }),
        );
        await this.prisma.$transaction(ops);
      }

      // remove hash before returning to caller
      const result: any = { ...user };
      delete result.hash;
      return result;
    } catch (e: any) {
      if (e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002' &&
        (e.meta?.target as string[]).includes('email')) {
        throw new ConflictException('Email already in use');
      }
      throw e;
    }
  }

  /**
   * Return all users with role and branch information.
   */
  async findAll(query: BootstrapTableRequestDto) {
    console.log('RAW query:', JSON.stringify(query))
    console.log('filter:', query.filter)
    const allowedFilters = {
      roleId: 'exact',
    } as const

    const where = buildWhereClause(
      query,
      allowedFilters,
      [
        "email", "firstName", "lastName"],  // global search on trxCode
    )

    const allowedSortColumns = ['email', 'firstName', 'lastName', 'createdAt', 'modifiedAt']

    const orderBy: Prisma.UserOrderByWithRelationInput = {}
    if (query.sort && allowedSortColumns.includes(query.sort)) {
      orderBy[query.sort] = query.sortOrder
    } else {
      orderBy.createdAt = 'desc'
    }

    // const users = await this.prisma.user.findMany({
    //   include: {
    //     role: true,
    //     branches: {
    //       include: {
    //         branch: true,
    //       },
    //     },
    //   },
    //   orderBy: { createdAt: 'desc' },
    // });

    const [total, rows] = await this.prisma.$transaction([
      this.prisma.user.count({ where }),
      this.prisma.user.findMany({
        where,
        skip: query.skip,
        take: query.take,
        orderBy,
        include: {
          role: true,
          branches: {
            include: {
              branch: true,
            },
          },
        },
      }),
    ])

    const rowsCleaned = rows.map((u) => {
      const result: any = { ...u };
      delete result.hash;
      return result;
    });

    return new BootstrapTableResponseDto(total, rowsCleaned)
  }

  async findOne(id: string) {
    const user = await this.prisma.user.findUnique({
      where: { id },
      include: {
        role: true,
        branches: {
          include: { branch: true },
        },
      },
    });
    if (user) {
      const result: any = { ...user };
      delete result.hash;
      return result;
    }
    return null;
  }

  async update(id: string, dto: UpdateUserDto) {
    const existing = await this.prisma.user.findUnique({ where: { id } });
    if (!existing) {
      throw new NotFoundException(`User with id ${id} not found`);
    }

    const data: any = {
      email: dto.email,
      firstName: dto.firstName,
      lastName: dto.lastName,
      roleId: dto.roleId,
    };

    if (dto.password) {
      data.hash = await argon.hash(dto.password);
    }

    // remove undefined props so prisma won't try to set them null
    Object.keys(data).forEach((k) => data[k] === undefined && delete data[k]);

    let updated;
    try {
      updated = await this.prisma.user.update({
        where: { id },
        data,
      });
    } catch (e: any) {
      if (e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002' &&
        (e.meta?.target as string[]).includes('email')) {
        throw new ConflictException('Email already in use');
      }
      throw e;
    }

    if (dto.branchIds) {
      // sync branches: delete those not in list, upsert provided ones
      await this.prisma.userBranch.deleteMany({
        where: { userId: id, branchId: { notIn: dto.branchIds } },
      });

      const ops = dto.branchIds.map((branchId) =>
        this.prisma.userBranch.upsert({
          where: {
            userId_branchId: { userId: id, branchId },
          },
          update: {},
          create: { userId: id, branchId },
        }),
      );
      await this.prisma.$transaction(ops);
    }

    const result: any = { ...updated };
    delete result.hash;
    return result;
  }

  remove(id: string) {
    return this.prisma.user.delete({
      where: { id },
    });
  }
}
