import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { randomUUID } from 'node:crypto';
import { normalizeLocale } from '@thebet365/shared';
import { RedisService } from '../../../shared/redis/redis.service';
import { PrismaService } from '../../../shared/prisma/prisma.service';
import { SystemConfigService } from '../../../shared/config/system-config.service';
import { appBadRequest, appForbidden, appTooManyRequests } from '../../../shared/common/app-error';
import { ChuanglanClient } from './chuanglan/client';
import { loadSmsBusinessConfig } from './chuanglan/config';
import { generateSixDigitCode } from './code';
import { normalizePhone, stripLocalPhoneDigits } from './phone.util';
import { renderResetPasswordSms, renderVerifySms } from './templates';
import type { SmsLang, SmsPurpose } from './sms.types';
import { maskPhoneForLog, shortSessionId } from './sms-log.util';

const codeKey = (sessionId: string) => `sms:code:${sessionId}`;
const phoneRateKey = (phone: string) => `sms:rate:phone:${phone}`;
const ipRateKey = (ip: string) => `sms:rate:ip:${ip}`;

function mapLocaleToSmsLang(locale?: string): SmsLang {
  const normalized = normalizeLocale(locale);
  if (normalized === 'zh-CN') return 'zh';
  if (normalized === 'ms-MY') return 'ms';
  return 'en';
}

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

  constructor(
    private redis: RedisService,
    private chuanglan: ChuanglanClient,
    private prisma: PrismaService,
    private systemConfig: SystemConfigService,
    config: ConfigService,
  ) {
    this.smsCfg = loadSmsBusinessConfig(config);
  }

  async sendVerifyCode(params: {
    phone: string;
    countryCode: string;
    locale?: string;
    clientIp: string;
    purpose?: SmsPurpose;
  }): Promise<{ sessionId: string }> {
    const purpose: SmsPurpose = params.purpose ?? 'register';
    const phone = normalizePhone(params.countryCode, params.phone);
    const maskedPhone = maskPhoneForLog(phone);

    this.logger.log(
      `SMS send request purpose=${purpose} phone=${maskedPhone} ip=${params.clientIp}`,
    );

    if (purpose === 'reset_password') {
      await this.assertPlayerCanResetPassword(params.countryCode, params.phone, maskedPhone);
    }

    const [phoneLimited, ipLimited] = await Promise.all([
      this.redis.exists(phoneRateKey(phone)),
      this.redis.exists(ipRateKey(params.clientIp)),
    ]);
    if (phoneLimited || ipLimited) {
      this.logger.warn(
        `SMS rate limited purpose=${purpose} phone=${maskedPhone} ip=${params.clientIp} phoneLimited=${phoneLimited} ipLimited=${ipLimited}`,
      );
      throw appTooManyRequests('SMS_RATE_LIMIT');
    }

    const code = generateSixDigitCode();
    const sessionId = randomUUID();
    const lang = mapLocaleToSmsLang(params.locale);
    const msg =
      purpose === 'reset_password'
        ? renderResetPasswordSms(lang, code)
        : renderVerifySms(lang, code);

    if (this.smsCfg.debugLogCode) {
      this.logger.warn(
        `[SMS_DEBUG] purpose=${purpose} phone=${phone} sessionId=${sessionId} code=${code} content=${msg}`,
      );
    }

    const result = await this.chuanglan.sendSms(phone, msg, sessionId);
    if (!result.success) {
      this.logger.error(
        `SMS send failed purpose=${purpose} phone=${maskedPhone} session=${shortSessionId(sessionId)} providerCode=${result.code} providerMessage=${result.message}`,
      );
      throw appBadRequest('SMS_SEND_FAILED');
    }

    await Promise.all([
      this.redis.set(
        codeKey(sessionId),
        JSON.stringify({ phone, code, purpose }),
        this.smsCfg.codeTtlSeconds,
      ),
      this.redis.set(phoneRateKey(phone), '1', this.smsCfg.rateLimitSeconds),
      this.redis.set(ipRateKey(params.clientIp), '1', this.smsCfg.rateLimitSeconds),
    ]);

    this.logger.log(
      `SMS sent purpose=${purpose} phone=${maskedPhone} session=${shortSessionId(sessionId)} messageId=${result.messageId ?? 'n/a'}`,
    );

    return { sessionId };
  }

  async verifyCode(params: {
    phone: string;
    countryCode: string;
    code: string;
    sessionId: string;
    expectedPurpose?: SmsPurpose;
  }): Promise<void> {
    const phone = normalizePhone(params.countryCode, params.phone);
    const maskedPhone = maskPhoneForLog(phone);
    const expectedPurpose = params.expectedPurpose ?? 'register';
    const session = shortSessionId(params.sessionId);

    this.logger.log(
      `SMS verify request purpose=${expectedPurpose} phone=${maskedPhone} session=${session}`,
    );

    const raw = await this.redis.get(codeKey(params.sessionId));
    if (!raw) {
      this.logger.warn(
        `SMS verify failed reason=expired purpose=${expectedPurpose} phone=${maskedPhone} session=${session}`,
      );
      throw appBadRequest('SMS_CODE_EXPIRED');
    }

    const cached = JSON.parse(raw) as { phone: string; code: string; purpose?: SmsPurpose };
    const cachedPurpose: SmsPurpose = cached.purpose ?? 'register';

    if (cachedPurpose !== expectedPurpose) {
      this.logger.warn(
        `SMS verify failed reason=purpose_mismatch expected=${expectedPurpose} cached=${cachedPurpose} phone=${maskedPhone} session=${session}`,
      );
      throw appBadRequest('SMS_CODE_INVALID');
    }
    if (cached.phone !== phone || cached.code !== params.code.trim()) {
      this.logger.warn(
        `SMS verify failed reason=invalid_code purpose=${expectedPurpose} phone=${maskedPhone} session=${session}`,
      );
      throw appBadRequest('SMS_CODE_INVALID');
    }

    await this.redis.del(codeKey(params.sessionId));

    this.logger.log(
      `SMS verify success purpose=${expectedPurpose} phone=${maskedPhone} session=${session}`,
    );
  }

  private async assertPlayerCanResetPassword(
    countryCode: string,
    phone: string,
    maskedPhone: string,
  ): Promise<void> {
    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' },
      },
      include: { user: { select: { status: true } } },
    });

    if (!pref) {
      this.logger.warn(`SMS reset_password blocked reason=phone_not_registered phone=${maskedPhone}`);
      throw appBadRequest('PHONE_NOT_REGISTERED');
    }
    if (pref.user.status === 'DISABLED') {
      this.logger.warn(`SMS reset_password blocked reason=account_disabled phone=${maskedPhone}`);
      throw appForbidden('ACCOUNT_DISABLED');
    }

    const settings = await this.systemConfig.getPlayerAccountSettings();
    if (!settings.allowPasswordChange) {
      this.logger.warn(
        `SMS reset_password blocked reason=password_change_disabled phone=${maskedPhone}`,
      );
      throw appForbidden('PASSWORD_CHANGE_DISABLED');
    }
  }
}
