import { Injectable, BadRequestException } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { SUPPORTED_LOCALES, isValidAvatarKey, assertPlayerUsername } from '@thebet365/shared';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { SystemConfigService } from '../../shared/config/system-config.service';
import { AgentsService } from '../agent/agents.service';
import { CashbackService } from '../operations/cashback/cashback.service';
import { PresenceService } from '../presence/presence.service';
import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error';
import { Decimal } from '@prisma/client/runtime/library';

export type PlayerListFilters = {
  keyword?: string;
  parentId?: bigint;
  platformDirect?: boolean;
  status?: string;
};

@Injectable()
export class UsersService {
  constructor(
    private prisma: PrismaService,
    private agents: AgentsService,
    private systemConfig: SystemConfigService,
    private cashback: CashbackService,
    private presence: PresenceService,
  ) {}

  private buildAffiliationAgents(
    parent?: {
      username: string;
      agentLevel: number | null;
      parent?: { username: string; agentLevel: number | null; parent?: unknown } | null;
    } | null,
    chain?: string[],
  ): string[] {
    if (chain?.length) return chain;
    if (!parent) return [];
    const ancestors: string[] = [];
    let cur: typeof parent | null | undefined = parent;
    while (cur) {
      ancestors.unshift(cur.username);
      cur = cur.parent as typeof parent | null | undefined;
    }
    return ancestors;
  }

  private async buildAffiliationChainMap(parentIds: (bigint | null | undefined)[]) {
    const map = new Map<string, string[]>();
    const pending = new Set<bigint>();
    const cache = new Map<string, { username: string; parentId: bigint | null }>();

    for (const pid of parentIds) {
      if (pid) pending.add(pid);
    }

    while (pending.size > 0) {
      const batch = [...pending];
      pending.clear();
      const agents = await this.prisma.user.findMany({
        where: { id: { in: batch }, userType: 'AGENT', deletedAt: null },
        select: { id: true, username: true, parentId: true },
      });
      for (const agent of agents) {
        cache.set(agent.id.toString(), { username: agent.username, parentId: agent.parentId });
        if (agent.parentId && !cache.has(agent.parentId.toString())) {
          pending.add(agent.parentId);
        }
      }
    }

    const build = (startId: bigint | null | undefined): string[] => {
      const chain: string[] = [];
      let cur = startId ?? null;
      while (cur) {
        const hit = cache.get(cur.toString());
        if (!hit) break;
        chain.unshift(hit.username);
        cur = hit.parentId;
      }
      return chain;
    };

    for (const pid of parentIds) {
      if (pid) map.set(pid.toString(), build(pid));
    }
    return map;
  }

  private formatPlayerRow(
    u: {
      id: bigint;
      username: string;
      status: string;
      locale: string;
      parentId: bigint | null;
      createdAt: Date;
      updatedAt: Date;
      wallet?: { availableBalance: { toString(): string }; frozenBalance: { toString(): string } } | null;
      preferences?: {
        phone: string | null;
        email: string | null;
        managedPassword?: string | null;
      } | null;
      parent?: {
        username: string;
        agentLevel: number | null;
        parent?: { username: string; agentLevel: number | null } | null;
      } | null;
      auth?: { lastLoginAt: Date | null } | null;
      usedInvite?: { code: string } | null;
    },
    bet?: { count: number; totalStake: string; totalReturn: string },
    affiliationChain?: string[],
  ) {
    const affiliationAgents = this.buildAffiliationAgents(u.parent, affiliationChain);
    return {
      id: u.id.toString(),
      username: u.username,
      status: u.status,
      locale: u.locale,
      parentId: u.parentId?.toString() ?? null,
      parentUsername: u.parent?.username ?? null,
      affiliationAgents,
      inviteCode: u.usedInvite?.code ?? null,
      phone: u.preferences?.phone ?? null,
      email: u.preferences?.email ?? null,
      managedPassword: u.preferences?.managedPassword ?? null,
      availableBalance: u.wallet?.availableBalance?.toString() ?? '0',
      frozenBalance: u.wallet?.frozenBalance?.toString() ?? '0',
      lastLoginAt: u.auth?.lastLoginAt ?? null,
      betCount: bet?.count ?? 0,
      totalStake: bet?.totalStake ?? '0',
      totalReturn: bet?.totalReturn ?? '0',
      createdAt: u.createdAt,
      updatedAt: u.updatedAt,
    };
  }

  private async loadBetStatsMap(userIds: bigint[]) {
    if (userIds.length === 0) return new Map<string, { count: number; totalStake: string; totalReturn: string }>();

    const groups = await this.prisma.bet.groupBy({
      by: ['userId'],
      where: { userId: { in: userIds } },
      _count: { _all: true },
      _sum: { stake: true, actualReturn: true },
    });

    return new Map(
      groups.map((g) => [
        g.userId.toString(),
        {
          count: g._count._all,
          totalStake: g._sum.stake?.toString() ?? '0',
          totalReturn: g._sum.actualReturn?.toString() ?? '0',
        },
      ]),
    );
  }

  async findById(id: bigint) {
    return this.prisma.user.findUnique({
      where: { id },
      include: { wallet: true, agentProfile: true, preferences: true },
    });
  }

  async updateProfile(
    userId: bigint,
    data: { phone?: string; email?: string; avatarKey?: string | null; username?: string },
  ) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      include: { preferences: true },
    });
    if (!user) throw appNotFound('USER_NOT_FOUND');

    if (data.username !== undefined) {
      const nextUsername = data.username.trim();
      if (!nextUsername) throw appBadRequest('USERNAME_REQUIRED');
      try {
        assertPlayerUsername(nextUsername);
      } catch {
        throw appBadRequest('USERNAME_FORMAT_INVALID');
      }
      const settings = await this.systemConfig.getPlayerAccountSettings();
      if (!settings.allowUsernameChange) {
        throw appForbidden('USERNAME_CHANGE_DISABLED');
      }
      if (nextUsername !== user.username) {
        const taken = await this.prisma.user.findUnique({ where: { username: nextUsername } });
        if (taken) throw appBadRequest('USERNAME_TAKEN');
        await this.prisma.user.update({
          where: { id: userId },
          data: { username: nextUsername },
        });
      }
    }

    const phone = data.phone !== undefined ? data.phone.trim() || null : undefined;
    const email = data.email !== undefined ? data.email.trim() || null : undefined;
    let avatarKey: string | null | undefined;
    if (data.avatarKey !== undefined) {
      avatarKey = data.avatarKey?.trim() || null;
      if (avatarKey && !isValidAvatarKey(avatarKey)) {
        throw appBadRequest('INVALID_AVATAR');
      }
    }

    const existing = await this.prisma.userPreference.findUnique({ where: { userId } });
    if (!existing && phone === undefined && email === undefined && avatarKey === undefined) {
      return this.findById(userId);
    }

    await this.prisma.userPreference.upsert({
      where: { userId },
      create: {
        userId,
        phone: phone ?? null,
        email: email ?? null,
        ...(avatarKey !== undefined ? { avatarKey } : {}),
      },
      update: {
        ...(phone !== undefined ? { phone } : {}),
        ...(email !== undefined ? { email } : {}),
        ...(avatarKey !== undefined ? { avatarKey } : {}),
      },
    });
    return this.findById(userId);
  }

  async updateLocale(userId: bigint, locale: string) {
    if (!(SUPPORTED_LOCALES as readonly string[]).includes(locale)) {
      throw appBadRequest('UNSUPPORTED_LOCALE');
    }
    await this.prisma.user.update({
      where: { id: userId },
      data: { locale },
    });
    await this.prisma.userPreference.upsert({
      where: { userId },
      create: { userId, locale },
      update: { locale },
    });
    return { locale };
  }

  async listPlayers(
    page = 1,
    pageSize = 10,
    filters: PlayerListFilters = {},
  ) {
    const where: {
      userType: string;
      deletedAt: null;
      parentId?: bigint | null;
      status?: string;
      OR?: { username?: { contains: string; mode: 'insensitive' } }[];
    } = {
      userType: 'PLAYER',
      deletedAt: null,
    };
    if (filters.platformDirect) {
      where.parentId = null;
    } else if (filters.parentId) {
      where.parentId = filters.parentId;
    }
    if (filters.status) where.status = filters.status;
    if (filters.keyword?.trim()) {
      const kw = filters.keyword.trim();
      where.OR = [{ username: { contains: kw, mode: 'insensitive' } }];
    }

    const skip = (page - 1) * pageSize;
    const [rows, total] = await Promise.all([
      this.prisma.user.findMany({
        where,
        include: {
          wallet: true,
          preferences: true,
          parent: {
            select: {
              id: true,
              username: true,
              agentLevel: true,
              parent: { select: { username: true, agentLevel: true } },
            },
          },
          auth: { select: { lastLoginAt: true } },
          usedInvite: { select: { code: true } },
        },
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
      }),
      this.prisma.user.count({ where }),
    ]);

    const betMap = await this.loadBetStatsMap(rows.map((r) => r.id));
    const affiliationMap = await this.buildAffiliationChainMap(rows.map((r) => r.parentId));
    const onlineSet = await this.presence.filterOnlineIds(rows.map((r) => r.id));
    return {
      items: rows.map((u) => {
        const row = this.formatPlayerRow(
          u,
          betMap.get(u.id.toString()),
          u.parentId ? affiliationMap.get(u.parentId.toString()) : undefined,
        );
        return {
          ...row,
          isOnline: onlineSet.has(row.id),
        };
      }),
      total,
      page,
      pageSize,
    };
  }

  async getPlayerAdminDetail(playerId: bigint) {
    const user = await this.prisma.user.findFirst({
      where: { id: playerId, userType: 'PLAYER', deletedAt: null },
      include: {
        wallet: true,
        preferences: true,
        parent: {
          select: {
            id: true,
            username: true,
            agentLevel: true,
            parent: { select: { username: true, agentLevel: true } },
          },
        },
        auth: { select: { lastLoginAt: true, loginFailCount: true, lockedUntil: true } },
        usedInvite: { select: { code: true } },
      },
    });
    if (!user) throw appNotFound('PLAYER_NOT_FOUND');

    const affiliationMap = await this.buildAffiliationChainMap([user.parentId]);
    const [betCount, betStake, customCashbackRate, defaultCashbackRate] = await Promise.all([
      this.prisma.bet.count({ where: { userId: playerId } }),
      this.prisma.bet.aggregate({
        where: { userId: playerId },
        _sum: { stake: true, actualReturn: true },
      }),
      this.cashback.getPlayerCustomCashbackRate(playerId),
      this.cashback.resolvePlayerDefaultCashbackRate({
        parentId: user.parentId,
        inviteSponsorId: user.inviteSponsorId,
      }),
    ]);

    return {
      ...this.formatPlayerRow(
        user,
        undefined,
        user.parentId ? affiliationMap.get(user.parentId.toString()) : undefined,
      ),
      lastLoginAt: user.auth?.lastLoginAt ?? null,
      loginFailCount: user.auth?.loginFailCount ?? 0,
      lockedUntil: user.auth?.lockedUntil ?? null,
      betCount,
      totalStake: betStake._sum.stake?.toString() ?? '0',
      totalReturn: betStake._sum.actualReturn?.toString() ?? '0',
      customCashbackRate: customCashbackRate?.toString() ?? null,
      defaultCashbackRate: defaultCashbackRate.toString(),
    };
  }

  async updatePlayerAdmin(
    playerId: bigint,
    data: {
      status?: string;
      locale?: string;
      phone?: string;
      email?: string;
      username?: string;
      password?: string;
      cashbackRate?: number | null;
    },
  ) {
    const user = await this.prisma.user.findFirst({
      where: { id: playerId, userType: 'PLAYER', deletedAt: null },
      include: { auth: true },
    });
    if (!user) throw appNotFound('PLAYER_NOT_FOUND');

    if (data.status && !['ACTIVE', 'SUSPENDED'].includes(data.status)) {
      throw appBadRequest('INVALID_STATUS');
    }

    if (data.username !== undefined) {
      const nextUsername = data.username.trim();
      if (!nextUsername) throw appBadRequest('USERNAME_REQUIRED');
      try {
        assertPlayerUsername(nextUsername);
      } catch {
        throw appBadRequest('USERNAME_FORMAT_INVALID');
      }
      if (nextUsername !== user.username) {
        const taken = await this.prisma.user.findUnique({ where: { username: nextUsername } });
        if (taken) throw appBadRequest('USERNAME_TAKEN');
        await this.prisma.user.update({
          where: { id: playerId },
          data: { username: nextUsername },
        });
      }
    }

    if (data.password !== undefined) {
      const nextPassword = data.password;
      if (nextPassword.length < 8) throw appBadRequest('PASSWORD_MIN_LENGTH');
      if (!user.auth) throw appBadRequest('AUTH_INFO_MISSING');
      const hash = await bcrypt.hash(nextPassword, 10);
      await this.prisma.userAuth.update({
        where: { userId: playerId },
        data: { passwordHash: hash, loginFailCount: 0, lockedUntil: null },
      });
      await this.prisma.userPreference.upsert({
        where: { userId: playerId },
        create: { userId: playerId, managedPassword: nextPassword },
        update: { managedPassword: nextPassword },
      });
    }

    if (data.status) {
      await this.prisma.user.update({
        where: { id: playerId },
        data: { status: data.status },
      });
    }

    if (data.locale) {
      await this.prisma.user.update({
        where: { id: playerId },
        data: { locale: data.locale },
      });
    }

    const prefPatch: {
      locale?: string;
      phone?: string | null;
      email?: string | null;
    } = {};

    if (data.locale) prefPatch.locale = data.locale;
    if (data.phone !== undefined) prefPatch.phone = data.phone.trim() || null;
    if (data.email !== undefined) prefPatch.email = data.email.trim() || null;

    if (Object.keys(prefPatch).length > 0) {
      await this.prisma.userPreference.upsert({
        where: { userId: playerId },
        create: {
          userId: playerId,
          locale: data.locale ?? user.locale,
          phone: prefPatch.phone ?? null,
          email: prefPatch.email ?? null,
        },
        update: prefPatch,
      });
    }

    if (data.cashbackRate !== undefined) {
      if (data.cashbackRate != null && (!Number.isFinite(data.cashbackRate) || data.cashbackRate < 0)) {
        throw appBadRequest('CASHBACK_RATE_NEGATIVE');
      }
      const rate =
        data.cashbackRate != null && data.cashbackRate > 0
          ? new Decimal(data.cashbackRate)
          : null;
      await this.cashback.setPlayerCustomCashbackRate(playerId, rate);
    }

    return this.getPlayerAdminDetail(playerId);
  }

  async getPlayerAccountPermissions() {
    return this.systemConfig.getPlayerAccountSettings();
  }

  async clearManagedPassword(userId: bigint) {
    const pref = await this.prisma.userPreference.findUnique({ where: { userId } });
    if (pref?.managedPassword) {
      await this.prisma.userPreference.update({
        where: { userId },
        data: { managedPassword: null },
      });
    }
  }

  async softDeletePlayer(playerId: bigint) {
    const user = await this.prisma.user.findFirst({
      where: { id: playerId, deletedAt: null },
    });
    if (!user) throw appNotFound('USER_NOT_FOUND');
    if (user.userType !== 'PLAYER') {
      throw appBadRequest('NOT_PLAYER');
    }
    // Block deletion when the player has any unresolved bets
    const betCount = await this.prisma.bet.count({
      where: {
        userId: playerId,
        status: 'PENDING',
      },
    });
    if (betCount > 0) {
      throw appBadRequest('PLAYER_HAS_PENDING_BETS');
    }
    // Block deletion when wallet still has balance
    const wallet = await this.prisma.wallet.findUnique({ where: { userId: playerId } });
    if (wallet) {
      const available = new Decimal(wallet.availableBalance);
      const frozen = new Decimal(wallet.frozenBalance);
      if (available.gt(0) || frozen.gt(0)) {
        throw appBadRequest('PLAYER_HAS_BALANCE');
      }
    }
    return this.prisma.user.update({
      where: { id: playerId },
      data: { deletedAt: new Date(), status: 'SUSPENDED' },
    });
  }
}
