import { HttpStatus, Injectable, type OnModuleInit } from '@nestjs/common';
import { DomainException, ErrorCode, LoggerService } from '@modules/shared';
import type {
  NotificationChannelPort,
  SendChannelMessageDto,
  SendChannelMessageResult,
} from '../../domain/ports/notification-channel.port';
import { type NotificationChannel } from '../../domain/value-objects/notification-channel.vo';
import {
  type SmsRateLimitBucket,
  SmsRateLimiterService,
} from './sms-rate-limiter.service';

export interface SendSmsDto {
  to: string;
  message: string;
  /** Rate-limit bucket; defaults to `transactional`. OTP flows should pass `otp`. */
  bucket?: SmsRateLimitBucket;
}

export interface SendOtpDto {
  to: string;
  code: string;
}

const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1000;
const OTP_TEMPLATE_KEYS = new Set([
  'user.phone_change_otp',
  'auth.login_otp',
  'auth.kyc_otp',
  'auth.phone_verify_otp',
]);

@Injectable()
export class StringeeSmsService implements OnModuleInit, NotificationChannelPort {
  readonly channel: NotificationChannel = 'SMS';

  private apiKey = '';
  private brandName = '';
  private initialized = false;
  private readonly baseUrl = 'https://api.stringee.com/v1/sms';

  constructor(
    private readonly logger: LoggerService,
    private readonly rateLimiter: SmsRateLimiterService,
  ) {}

  onModuleInit(): void {
    this.apiKey = process.env['STRINGEE_API_KEY'] ?? '';
    this.brandName = process.env['STRINGEE_BRANDNAME'] ?? 'GoodGo';

    if (!this.apiKey) {
      this.logger.warn(
        'STRINGEE_API_KEY not set — SMS notifications disabled',
        'StringeeSmsService',
      );
      return;
    }

    this.initialized = true;
    this.logger.log(
      `Stringee SMS configured with brandname "${this.brandName}"`,
      'StringeeSmsService',
    );
  }

  get isAvailable(): boolean {
    return this.initialized;
  }

  async sendOTP(dto: SendOtpDto): Promise<SendChannelMessageResult> {
    const message = `[${this.brandName}] Ma xac thuc cua ban la: ${dto.code}. Ma co hieu luc trong 5 phut.`;
    return this.dispatch({ to: dto.to, message, bucket: 'otp' });
  }

  async sendNotification(dto: SendSmsDto): Promise<SendChannelMessageResult> {
    return this.dispatch(dto);
  }

  async send(dto: SendChannelMessageDto): Promise<SendChannelMessageResult> {
    const bucket: SmsRateLimitBucket = OTP_TEMPLATE_KEYS.has(dto.templateKey) ? 'otp' : 'transactional';
    const plainText = this.stripHtml(dto.body);
    return this.dispatch({ to: dto.recipient, message: plainText, bucket });
  }

  private async dispatch(dto: SendSmsDto): Promise<SendChannelMessageResult> {
    if (!this.initialized) {
      throw new Error('Stringee SMS not initialized — STRINGEE_API_KEY not configured');
    }

    const phone = this.normalizePhone(dto.to);
    const bucket: SmsRateLimitBucket = dto.bucket ?? 'transactional';

    await this.enforceRateLimit(phone, bucket);

    return this.sendWithRetry(phone, dto.message);
  }

  private async enforceRateLimit(phone: string, bucket: SmsRateLimitBucket): Promise<void> {
    const perMinute = await this.rateLimiter.check(phone, bucket);
    if (!perMinute.allowed) {
      throw new DomainException(
        ErrorCode.TOO_MANY_REQUESTS,
        `SMS rate limit exceeded. Retry after ${perMinute.retryAfterSeconds}s.`,
        HttpStatus.TOO_MANY_REQUESTS,
        { bucket: perMinute.bucket, retryAfterSeconds: perMinute.retryAfterSeconds },
      );
    }

    const hourlyBucket: SmsRateLimitBucket = bucket === 'otp' ? 'otpHourly' : 'transactionalHourly';
    const perHour = await this.rateLimiter.check(phone, hourlyBucket);
    if (!perHour.allowed) {
      throw new DomainException(
        ErrorCode.TOO_MANY_REQUESTS,
        `Hourly SMS limit exceeded. Retry after ${perHour.retryAfterSeconds}s.`,
        HttpStatus.TOO_MANY_REQUESTS,
        { bucket: perHour.bucket, retryAfterSeconds: perHour.retryAfterSeconds },
      );
    }
  }

  private async sendWithRetry(phone: string, message: string): Promise<SendChannelMessageResult> {
    let lastError: Error | undefined;

    for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
      try {
        return await this.postToStringee(phone, message);
      } catch (error) {
        lastError = error instanceof Error ? error : new Error(String(error));

        if (attempt < MAX_RETRIES) {
          const delayMs = BASE_DELAY_MS * Math.pow(2, attempt - 1);
          this.logger.warn(
            `Stringee SMS attempt ${attempt}/${MAX_RETRIES} failed: ${lastError.message}. Retrying in ${delayMs}ms...`,
            'StringeeSmsService',
          );
          await this.delay(delayMs);
        }
      }
    }

    this.logger.error(
      `Stringee SMS failed after ${MAX_RETRIES} attempts: ${lastError?.message}`,
      'StringeeSmsService',
    );
    throw lastError;
  }

  private async postToStringee(phone: string, message: string): Promise<SendChannelMessageResult> {
    const body = {
      from: { type: 'sms', number: this.brandName, alias: this.brandName },
      to: [{ type: 'sms', number: phone }],
      text: message,
    };

    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-STRINGEE-AUTH': this.apiKey,
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      const errorText = await response.text().catch(() => 'Unknown error');
      throw new Error(`Stringee API error (${response.status}): ${errorText}`);
    }

    const data = (await response.json()) as { message_id?: string; r?: number; message?: string };

    if (data.r !== undefined && data.r !== 0) {
      throw new Error(`Stringee SMS rejected (code ${data.r}): ${data.message ?? 'Unknown reason'}`);
    }

    const messageId = data.message_id ?? `stringee-${Date.now()}`;

    this.logger.log(
      `SMS sent to ${phone.slice(0, 6)}***: ${messageId}`,
      'StringeeSmsService',
    );

    return { messageId };
  }

  private normalizePhone(phone: string): string {
    const cleaned = phone.replace(/[\s\-()]/g, '');

    if (cleaned.startsWith('+84')) {
      return cleaned;
    }
    if (cleaned.startsWith('84') && cleaned.length >= 11) {
      return `+${cleaned}`;
    }
    if (cleaned.startsWith('0')) {
      return `+84${cleaned.slice(1)}`;
    }
    return cleaned;
  }

  private stripHtml(html: string): string {
    return html.replace(/<[^>]*>/g, '').trim();
  }

  private delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}
