import { Injectable, Logger } from '@nestjs/common';
import { Decimal } from '@prisma/client/runtime/library';
import { appForbidden, appUnauthorized, appBadRequest, appNotFound } from '../../shared/common/app-error';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcryptjs';
import { assertPlayerUsername } from '@thebet365/shared';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { SystemConfigService } from '../../shared/config/system-config.service';
import { InvitesService } from './invites.service';
import { SmsService } from './sms/sms.service';
import { AuditService } from '../operations/audit/audit.service';
import { normalizePhone, resolvePlayerLoginCandidates, stripLocalPhoneDigits } from './sms/phone.util';
import { maskPhoneForLog } from './sms/sms-log.util';

const MAX_LOGIN_FAILS = 5;
const LOCK_DURATION_MS = 15 * 60 * 1000;

export interface JwtPayload {
  sub: string;
  username: string;
  userType: string;
  role?: string;
}

@Injectable()
export class AuthService {
  private readonly logger = new Logger(AuthService.name);

  constructor(
    private prisma: PrismaService,
    private jwt: JwtService,
    private config: ConfigService,
    private systemConfig: SystemConfigService,
    private invites: InvitesService,
    private sms: SmsService,
    private audit: AuditService,
  ) {}

  /** 平台管理员 / 代理统一登录（按 userType 签发对应 JWT） */
  async staffLogin(username: string, password: string) {
    const user = await this.prisma.user.findUnique({
      where: { username },
      select: { userType: true },
    });
    if (!user || (user.userType !== 'ADMIN' && user.userType !== 'AGENT')) {
      throw appUnauthorized('INVALID_CREDENTIALS');
    }
    const portal = user.userType === 'ADMIN' ? 'admin' : 'agent';
    return this.login(username, password, portal);
  }

  async login(
    username: string,
    password: string,
    portal: 'player' | 'admin' | 'agent',
    countryCode?: string,
  ) {
    let user;
    if (portal === 'player') {
      const trimmed = username.trim();
      if (/[a-zA-Z]/.test(trimmed)) {
        user = await this.prisma.user.findUnique({
          where: { username: trimmed },
          include: { auth: true, adminRole: { include: { role: true } } },
        });
      } else {
        const candidates = resolvePlayerLoginCandidates(username, countryCode);
        let matches = await this.prisma.user.findMany({
          where: {
            username: { in: candidates },
            userType: 'PLAYER',
            deletedAt: null,
          },
          include: { auth: true, adminRole: { include: { role: true } } },
        });

        if (matches.length === 0 && countryCode?.trim()) {
          const dial = countryCode.replace(/\D/g, '');
          const local = stripLocalPhoneDigits(username);
          matches = await this.prisma.user.findMany({
            where: {
              userType: 'PLAYER',
              deletedAt: null,
              preferences: {
                phoneCountryDial: dial,
                phoneLocal: local,
              },
            },
            include: { auth: true, adminRole: { include: { role: true } } },
          });
        }

        if (matches.length === 1) {
          user = matches[0];
        }
      }
    } else {
      user = await this.prisma.user.findUnique({
        where: { username: username.trim() },
        include: {
          auth: true,
          adminRole: {
            include: {
              role: {
                include: {
                  permissions: { include: { permission: true } },
                },
              },
            },
          },
        },
      });
    }

    if (!user || !user.auth) {
      throw appUnauthorized('INVALID_CREDENTIALS');
    }

    const expectedType = portal === 'admin' ? 'ADMIN' : portal === 'agent' ? 'AGENT' : 'PLAYER';
    if (user.userType !== expectedType) {
      throw appUnauthorized('INVALID_CREDENTIALS');
    }

    if (user.status === 'DISABLED') {
      throw appForbidden('ACCOUNT_DISABLED');
    }

    if (portal === 'agent' && user.status === 'SUSPENDED') {
      throw appForbidden('AGENT_ACCOUNT_SUSPENDED');
    }

    if (portal === 'player' && user.status === 'SUSPENDED') {
      throw appForbidden('ACCOUNT_SUSPENDED');
    }

    if (portal === 'player' && user.parentId) {
      const parentAgent = await this.prisma.user.findUnique({
        where: { id: user.parentId },
        select: {
          userType: true,
          status: true,
          agentProfile: { select: { blockDirectPlayerLogin: true } },
        },
      });
      if (
        parentAgent?.userType === 'AGENT' &&
        parentAgent.status !== 'ACTIVE' &&
        parentAgent.agentProfile?.blockDirectPlayerLogin
      ) {
        throw appForbidden('PARENT_AGENT_SUSPENDED');
      }
    }

    if (user.auth.lockedUntil && user.auth.lockedUntil > new Date()) {
      throw appForbidden('ACCOUNT_LOCKED');
    }

    const valid = await bcrypt.compare(password, user.auth.passwordHash);
    if (!valid) {
      const failCount = user.auth.loginFailCount + 1;
      const lockedUntil =
        failCount >= MAX_LOGIN_FAILS ? new Date(Date.now() + LOCK_DURATION_MS) : null;
      await this.prisma.userAuth.update({
        where: { userId: user.id },
        data: { loginFailCount: failCount, lockedUntil },
      });
      throw appUnauthorized('INVALID_CREDENTIALS');
    }

    await this.prisma.userAuth.update({
      where: { userId: user.id },
      data: { loginFailCount: 0, lockedUntil: null, lastLoginAt: new Date() },
    });

    const expiresIn =
      portal === 'admin'
        ? this.config.get('JWT_ADMIN_EXPIRES', '2h')
        : portal === 'agent'
          ? this.config.get('JWT_AGENT_EXPIRES', '8h')
          : this.config.get('JWT_PLAYER_EXPIRES', '24h');

    const payload: JwtPayload = {
      sub: user.id.toString(),
      username: user.username,
      userType: user.userType,
      role: user.adminRole?.role?.code,
    };

    const token = this.jwt.sign(payload, { expiresIn });

    const rolePerms = user.adminRole?.role as
      | { permissions?: Array<{ permission: { code: string } }> }
      | undefined
      | null;
    const adminPermissions =
      user.userType === 'ADMIN'
        ? (rolePerms?.permissions?.map((rp) => rp.permission.code) ?? [])
        : undefined;

    return {
      token,
      user: {
        id: user.id.toString(),
        username: user.username,
        userType: user.userType,
        locale: user.locale,
        role: user.adminRole?.role?.code,
        agentLevel: user.userType === 'AGENT' ? user.agentLevel : null,
        visibleMenus: user.visibleMenus,
        ...(adminPermissions ? { permissions: adminPermissions } : {}),
      },
    };
  }

  async resolveInviteSponsor(inviteCodeRaw?: string | null) {
    const resolved = await this.invites.resolveActiveInvite(inviteCodeRaw);
    return {
      sponsorId: resolved.sponsorId,
      parentId: resolved.parentId,
      inviteId: resolved.inviteId,
    };
  }

  async registerPlayer(data: {
    username: string;
    phone: string;
    countryCode: string;
    password: string;
    smsCode: string;
    sessionId: string;
    inviteCode?: string;
    locale?: string;
  }) {
    const username = data.username.trim();
    if (!username) {
      throw appBadRequest('USERNAME_REQUIRED');
    }
    try {
      assertPlayerUsername(username);
    } catch {
      throw appBadRequest('USERNAME_FORMAT_INVALID');
    }

    const dial = data.countryCode.replace(/\D/g, '');
    const phoneLocal = stripLocalPhoneDigits(data.phone);
    const phone = normalizePhone(dial, data.phone);

    if (!data.password || data.password.length < 8) {
      throw appBadRequest('PASSWORD_MIN_LENGTH');
    }
    if (!data.smsCode?.trim() || !data.sessionId?.trim()) {
      throw appBadRequest('SMS_CODE_REQUIRED');
    }

    await this.sms.verifyCode({
      phone: data.phone,
      countryCode: data.countryCode,
      code: data.smsCode.trim(),
      sessionId: data.sessionId.trim(),
      expectedPurpose: 'register',
    });

    const { parentId, sponsorId, inviteId } = await this.resolveInviteSponsor(data.inviteCode);
    const inviteSponsorId = parentId == null && sponsorId != null ? sponsorId : null;

    const existingUser = await this.prisma.user.findUnique({
      where: { username },
      select: { id: true },
    });
    if (existingUser) {
      throw appBadRequest('USERNAME_TAKEN');
    }

    const existingPhone = await this.prisma.userPreference.findFirst({
      where: {
        phoneCountryDial: dial,
        phoneLocal,
        user: { deletedAt: null, userType: 'PLAYER' },
      },
      select: { userId: true },
    });
    if (existingPhone) {
      throw appBadRequest('PHONE_TAKEN');
    }

    const hash = await this.hashPassword(data.password);
    const locale = data.locale?.trim() || 'zh-CN';

    const user = await this.prisma.$transaction(async (tx) => {
      const created = await tx.user.create({
        data: {
          username,
          userType: 'PLAYER',
          parentId,
          inviteSponsorId,
          locale,
        },
      });

      await tx.userAuth.create({
        data: { userId: created.id, passwordHash: hash },
      });

      await tx.wallet.create({
        data: { userId: created.id },
      });

      await tx.userPreference.create({
        data: {
          userId: created.id,
          locale,
          phone,
          phoneCountryDial: dial,
          phoneLocal,
        },
      });

      if (inviteId) {
        await this.invites.recordRegistration(inviteId, created.id, tx);
        const invite = await tx.userInvite.findUnique({
          where: { id: inviteId },
          select: { cashbackRate: true },
        });
        if (invite?.cashbackRate != null && new Decimal(invite.cashbackRate).gt(0)) {
          await tx.cashbackRule.updateMany({
            where: { targetType: 'USER', targetId: created.id },
            data: { isActive: false },
          });
          await tx.cashbackRule.create({
            data: {
              name: `Player ${created.id.toString()}`,
              targetType: 'USER',
              targetId: created.id,
              rate: invite.cashbackRate,
              isActive: true,
            },
          });
        }
      }

      return created;
    });

    this.logger.log(`Player registered username=${username} phone=${maskPhoneForLog(phone)}`);

    return this.login(username, data.password, 'player');
  }

  async getInviteInfo(userId: bigint) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { inviteCode: true, 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');
    }
    return { inviteCode: user.inviteCode ?? null };
  }

  async generateInviteCode(
    userId: bigint,
    userType: string,
    cashbackRate?: number,
  ) {
    return this.invites.generateInviteCode(userId, {
      userType,
      cashbackRate: cashbackRate ?? null,
    });
  }

  async changePassword(userId: bigint, oldPassword: string, newPassword: string) {
    const auth = await this.prisma.userAuth.findUnique({ where: { userId } });
    if (!auth) throw appUnauthorized('USER_NOT_FOUND');

    const settings = await this.systemConfig.getPlayerAccountSettings();
    if (!settings.allowPasswordChange) {
      throw appForbidden('PASSWORD_CHANGE_DISABLED');
    }

    const valid = await bcrypt.compare(oldPassword, auth.passwordHash);
    if (!valid) throw appUnauthorized('INVALID_OLD_PASSWORD');

    const hash = await bcrypt.hash(newPassword, 10);
    await this.prisma.userAuth.update({
      where: { userId },
      data: { passwordHash: hash },
    });
    await this.prisma.userPreference.updateMany({
      where: { userId },
      data: { managedPassword: null },
    });
    return { success: true };
  }

  async resetPasswordByPhone(data: {
    phone: string;
    countryCode: string;
    smsCode: string;
    sessionId: string;
    newPassword: string;
    ipAddress?: string;
  }) {
    const settings = await this.systemConfig.getPlayerAccountSettings();
    if (!settings.allowPasswordChange) {
      throw appForbidden('PASSWORD_CHANGE_DISABLED');
    }

    if (!data.newPassword || data.newPassword.length < 8) {
      throw appBadRequest('PASSWORD_MIN_LENGTH');
    }
    if (!data.smsCode?.trim() || !data.sessionId?.trim()) {
      throw appBadRequest('SMS_CODE_REQUIRED');
    }

    await this.sms.verifyCode({
      phone: data.phone,
      countryCode: data.countryCode,
      code: data.smsCode.trim(),
      sessionId: data.sessionId.trim(),
      expectedPurpose: 'reset_password',
    });

    const player = await this.findPlayerByPhone(data.countryCode, data.phone);
    if (!player) {
      throw appBadRequest('PHONE_NOT_REGISTERED');
    }
    if (player.status === 'DISABLED') {
      throw appForbidden('ACCOUNT_DISABLED');
    }

    const hash = await this.hashPassword(data.newPassword);
    await this.prisma.userAuth.update({
      where: { userId: player.id },
      data: {
        passwordHash: hash,
        loginFailCount: 0,
        lockedUntil: null,
      },
    });
    await this.prisma.userPreference.updateMany({
      where: { userId: player.id },
      data: { managedPassword: null },
    });

    await this.audit.log({
      operatorId: player.id,
      operatorType: 'PLAYER',
      action: 'FORGOT_PASSWORD_RESET',
      module: 'identity',
      targetType: 'user',
      targetId: player.id.toString(),
      ipAddress: data.ipAddress,
    });

    this.logger.log(
      `Password reset by phone userId=${player.id.toString()} phone=${maskPhoneForLog(normalizePhone(data.countryCode, data.phone))} ip=${data.ipAddress ?? 'n/a'}`,
    );

    return { success: true };
  }

  private async findPlayerByPhone(countryCode: string, phone: string) {
    const dial = countryCode.replace(/\D/g, '');
    const phoneLocal = stripLocalPhoneDigits(phone);
    const pref = await this.prisma.userPreference.findFirst({
      where: {
        phoneCountryDial: dial,
        phoneLocal,
        user: { deletedAt: null, userType: 'PLAYER' },
      },
      select: { user: { select: { id: true, status: true } } },
    });
    return pref?.user ?? null;
  }

  async hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, 10);
  }
}
