import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error';
import { PrismaService } from '../../shared/prisma/prisma.service';
import {
  INVITE_STATUS_ACTIVE,
  INVITE_STATUS_REVOKED,
  normalizeInviteCode,
  rotateUserInviteCode,
} from '../../shared/common/invite-code.util';

export interface InviteListQuery {
  page?: number;
  pageSize?: number;
  status?: string;
  sponsorId?: string;
  keyword?: string;
}

function formatInviteRow(row: {
  id: bigint;
  code: string;
  status: string;
  createdAt: Date;
  revokedAt: Date | null;
  cashbackRate: Prisma.Decimal | null;
  sponsor: {
    id: bigint;
    username: string;
    userType: string;
    agentLevel: number | null;
  };
  registrations?: { id: bigint; username: string }[];
}) {
  const registrant = row.registrations?.[0] ?? null;
  return {
    id: row.id.toString(),
    code: row.code,
    status: row.status,
    createdAt: row.createdAt.toISOString(),
    revokedAt: row.revokedAt?.toISOString() ?? null,
    cashbackRate: row.cashbackRate?.toString() ?? null,
    sponsorId: row.sponsor.id.toString(),
    sponsorUsername: row.sponsor.username,
    sponsorUserType: row.sponsor.userType,
    sponsorAgentLevel: row.sponsor.agentLevel,
    registeredPlayerId: registrant ? registrant.id.toString() : null,
    registeredPlayerUsername: registrant?.username ?? null,
  };
}

const inviteListInclude = {
  sponsor: {
    select: { id: true, username: true, userType: true, agentLevel: true },
  },
  registrations: {
    where: { userType: 'PLAYER', deletedAt: null },
    select: { id: true, username: true },
    take: 1,
  },
} as const;

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

  async getSelfAndDescendantIds(agentId: bigint): Promise<bigint[]> {
    const rows = await this.prisma.agentClosure.findMany({
      where: { ancestorId: agentId },
      select: { descendantId: true },
    });
    return rows.map((r) => r.descendantId);
  }

  private async assertSponsorInScope(
    operator: { id: bigint; userType: string },
    sponsorId: bigint,
  ) {
    if (operator.userType === 'ADMIN') return;
    if (operator.userType !== 'AGENT') {
      throw appForbidden('ACCESS_DENIED_PORTAL');
    }
    const allowed = await this.getSelfAndDescendantIds(operator.id);
    if (!allowed.some((id) => id === sponsorId)) {
      throw appForbidden('ACCESS_DENIED_PORTAL');
    }
  }

  async resolveActiveInvite(inviteCodeRaw?: string | null) {
    const inviteCode = normalizeInviteCode(inviteCodeRaw ?? '');
    if (!inviteCode) {
      return { inviteId: null as bigint | null, sponsorId: null as bigint | null, parentId: null as bigint | null };
    }

    const invite = await this.prisma.userInvite.findUnique({
      where: { code: inviteCode },
      include: {
        sponsor: {
          select: {
            id: true,
            userType: true,
            status: true,
            deletedAt: true,
            agentProfile: { select: { blockDirectPlayerLogin: true } },
          },
        },
        registrations: {
          where: { deletedAt: null },
          select: { id: true },
          take: 1,
        },
      },
    });

    if (!invite || invite.status !== INVITE_STATUS_ACTIVE) {
      throw appBadRequest('INVITE_CODE_INVALID');
    }
    if (invite.registrations.length > 0) {
      throw appBadRequest('INVITE_CODE_ALREADY_USED');
    }

    const sponsor = invite.sponsor;
    if (!sponsor || sponsor.deletedAt) {
      throw appBadRequest('INVITE_CODE_INVALID');
    }
    if (sponsor.userType !== 'ADMIN' && sponsor.userType !== 'AGENT') {
      throw appBadRequest('INVITE_CODE_INVALID');
    }
    if (sponsor.status === 'DISABLED') {
      throw appBadRequest('INVITE_CODE_NOT_AVAILABLE');
    }
    if (sponsor.userType === 'AGENT') {
      if (sponsor.status === 'SUSPENDED') {
        throw appBadRequest('INVITE_CODE_NOT_AVAILABLE');
      }
      if (sponsor.agentProfile?.blockDirectPlayerLogin) {
        throw appBadRequest('INVITE_CODE_NOT_AVAILABLE');
      }
    }

    return {
      inviteId: invite.id,
      sponsorId: sponsor.id,
      parentId: sponsor.userType === 'AGENT' ? sponsor.id : null,
    };
  }

  async recordRegistration(inviteId: bigint, playerId: bigint, tx: Prisma.TransactionClient) {
    const invite = await tx.userInvite.findUnique({
      where: { id: inviteId },
      include: {
        sponsor: { select: { inviteCode: true } },
        registrations: {
          where: { deletedAt: null },
          select: { id: true },
          take: 1,
        },
      },
    });
    if (!invite) {
      throw appBadRequest('INVITE_CODE_INVALID');
    }
    if (invite.registrations.length > 0) {
      throw appBadRequest('INVITE_CODE_ALREADY_USED');
    }

    await tx.user.update({
      where: { id: playerId },
      data: { usedInviteId: inviteId },
    });

    const now = new Date();
    await tx.userInvite.update({
      where: { id: inviteId },
      data: {
        registerCount: 1,
        status: INVITE_STATUS_REVOKED,
        revokedAt: now,
      },
    });

    if (invite.sponsor.inviteCode === invite.code) {
      await tx.user.update({
        where: { id: invite.sponsorId },
        data: { inviteCode: null },
      });
    }
  }

  async generateInviteCode(
    userId: bigint,
    options?: { userType?: string; cashbackRate?: number | null },
  ) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { userType: true, deletedAt: true },
    });
    if (!user || user.deletedAt) {
      throw appNotFound('USER_NOT_FOUND');
    }
    if (user.userType !== 'ADMIN' && user.userType !== 'AGENT') {
      throw appForbidden('ACCESS_DENIED_PORTAL');
    }

    let cashbackRate: Prisma.Decimal | null = null;
    if (options?.cashbackRate != null) {
      if (user.userType !== 'ADMIN') {
        throw appForbidden('ACCESS_DENIED_PORTAL');
      }
      if (!Number.isFinite(options.cashbackRate) || options.cashbackRate < 0) {
        throw appBadRequest('INVITE_CASHBACK_RATE_INVALID');
      }
      cashbackRate = new Prisma.Decimal(options.cashbackRate);
    }

    const inviteCode = await rotateUserInviteCode(this.prisma, userId, cashbackRate);
    return {
      inviteCode,
      cashbackRate: cashbackRate?.toString() ?? null,
    };
  }

  async listInvites(
    operator: { id: bigint; userType: string },
    query: InviteListQuery,
  ) {
    const page = Math.max(1, query.page ?? 1);
    const pageSize = Math.min(100, Math.max(1, query.pageSize ?? 20));
    const where: Prisma.UserInviteWhereInput = {};

    if (query.status === INVITE_STATUS_ACTIVE || query.status === INVITE_STATUS_REVOKED) {
      where.status = query.status;
    }

    const keyword = query.keyword?.trim();
    if (keyword) {
      where.code = { contains: normalizeInviteCode(keyword) };
    }

    if (operator.userType === 'ADMIN') {
      if (query.sponsorId?.trim()) {
        where.sponsorId = BigInt(query.sponsorId.trim());
      }
    } else if (operator.userType === 'AGENT') {
      const allowedIds = await this.getSelfAndDescendantIds(operator.id);
      if (query.sponsorId?.trim()) {
        const sid = BigInt(query.sponsorId.trim());
        if (!allowedIds.some((id) => id === sid)) {
          throw appForbidden('ACCESS_DENIED_PORTAL');
        }
        where.sponsorId = sid;
      } else {
        where.sponsorId = { in: allowedIds };
      }
    } else {
      throw appForbidden('ACCESS_DENIED_PORTAL');
    }

    const [total, rows] = await Promise.all([
      this.prisma.userInvite.count({ where }),
      this.prisma.userInvite.findMany({
        where,
        include: inviteListInclude,
        orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
        skip: (page - 1) * pageSize,
        take: pageSize,
      }),
    ]);

    return {
      items: rows.map(formatInviteRow),
      total,
      page,
      pageSize,
    };
  }

  async listSponsorOptions(operator: { id: bigint; userType: string }) {
    if (operator.userType === 'ADMIN') {
      const rows = await this.prisma.user.findMany({
        where: {
          userType: { in: ['ADMIN', 'AGENT'] },
          deletedAt: null,
        },
        select: { id: true, username: true, userType: true, agentLevel: true },
        orderBy: [{ userType: 'asc' }, { username: 'asc' }],
      });
      return rows.map((r) => ({
        id: r.id.toString(),
        username: r.username,
        userType: r.userType,
        agentLevel: r.agentLevel,
      }));
    }

    if (operator.userType !== 'AGENT') {
      throw appForbidden('ACCESS_DENIED_PORTAL');
    }

    const ids = await this.getSelfAndDescendantIds(operator.id);
    const rows = await this.prisma.user.findMany({
      where: { id: { in: ids }, deletedAt: null },
      select: { id: true, username: true, userType: true, agentLevel: true },
      orderBy: [{ agentLevel: 'asc' }, { username: 'asc' }],
    });
    return rows.map((r) => ({
      id: r.id.toString(),
      username: r.username,
      userType: r.userType,
      agentLevel: r.agentLevel,
    }));
  }

  async revokeInvite(
    operator: { id: bigint; userType: string },
    inviteId: bigint,
  ) {
    const invite = await this.prisma.userInvite.findUnique({
      where: { id: inviteId },
      include: { sponsor: { select: { id: true, inviteCode: true } } },
    });
    if (!invite) {
      throw appNotFound('INVITE_NOT_FOUND');
    }
    if (invite.status === INVITE_STATUS_REVOKED) {
      const full = await this.prisma.userInvite.findUnique({
        where: { id: inviteId },
        include: inviteListInclude,
      });
      if (!full) throw appNotFound('INVITE_NOT_FOUND');
      return formatInviteRow(full);
    }

    await this.assertSponsorInScope(operator, invite.sponsorId);

    if (operator.userType === 'AGENT' && invite.sponsorId !== operator.id) {
      throw appForbidden('ACCESS_DENIED_PORTAL');
    }

    const updated = await this.prisma.$transaction(async (tx) => {
      const row = await tx.userInvite.update({
        where: { id: inviteId },
        data: { status: INVITE_STATUS_REVOKED, revokedAt: new Date() },
        include: {
          sponsor: {
            select: { id: true, username: true, userType: true, agentLevel: true, inviteCode: true },
          },
          registrations: {
            where: { userType: 'PLAYER', deletedAt: null },
            select: { id: true, username: true },
            take: 1,
          },
        },
      });

      if (row.sponsor.inviteCode === row.code) {
        await tx.user.update({
          where: { id: row.sponsorId },
          data: { inviteCode: null },
        });
      }

      return row;
    });

    return formatInviteRow(updated);
  }

  async deleteInvite(
    operator: { id: bigint; userType: string },
    inviteId: bigint,
  ) {
    const invite = await this.prisma.userInvite.findUnique({
      where: { id: inviteId },
      select: { id: true, status: true, sponsorId: true },
    });
    if (!invite) {
      throw appNotFound('INVITE_NOT_FOUND');
    }
    if (invite.status !== INVITE_STATUS_REVOKED) {
      throw appBadRequest('INVITE_MUST_REVOKE_FIRST');
    }

    const usedBy = await this.prisma.user.findFirst({
      where: { usedInviteId: inviteId, deletedAt: null },
      select: { id: true },
    });
    if (usedBy) {
      throw appBadRequest('INVITE_CANNOT_DELETE_USED');
    }

    await this.assertSponsorInScope(operator, invite.sponsorId);

    if (operator.userType === 'AGENT' && invite.sponsorId !== operator.id) {
      throw appForbidden('ACCESS_DENIED_PORTAL');
    }

    await this.prisma.userInvite.delete({ where: { id: inviteId } });
    return { success: true };
  }
}
