import { createHash } from 'node:crypto';

import { Injectable, Logger } from '@nestjs/common';

import { ConfigService } from '@nestjs/config';

import { SettingsService } from '../domain/settings.service';



interface ModemSessionTokens {

  sesInfo: string;

  tokInfo: string;

}



interface ModemCredentials {

  username: string;

  password: string;

}



@Injectable()

export class SmsService {

  private readonly logger = new Logger(SmsService.name);

  private readonly requestTimeoutMs = 15_000;

  private readonly logTokenChunkSize = 60;



  constructor(

    private readonly settings: SettingsService,

    private readonly config: ConfigService

  ) {}



  async sendOtp(phoneNumber: string, messageText: string): Promise<boolean> {

    let modemUrl = '';

    try {

      modemUrl = await this.resolveModemUrl();

      const tokens = await this.fetchSessionTokens(modemUrl);

      await this.postSms(modemUrl, tokens, phoneNumber, messageText);

      this.logger.log(`SMS отправлено через Huawei HiLink на ${this.maskPhone(phoneNumber)}`);

      return true;

    } catch (error) {

      const message = this.formatError(error, modemUrl);

      this.logger.error(`Не удалось отправить SMS через Huawei HiLink: ${message}`);

      return false;

    }

  }



  private async resolveModemUrl(): Promise<string> {

    const fromDb = (await this.settings.getValue('SMS_GATEWAY_URL', '')).trim();

    if (fromDb) {

      return this.normalizeModemUrl(fromDb);

    }



    const fromEnv = (this.config.get<string>('HUAWEI_MODEM_URL') ?? '').trim();

    if (fromEnv) {

      return this.normalizeModemUrl(fromEnv);

    }



    return 'http://192.168.8.1';

  }



  private async resolveModemCredentials(): Promise<ModemCredentials> {

    const usernameFromDb = (await this.settings.getValue('SMS_GATEWAY_USERNAME', '')).trim();

    const passwordFromDb = (await this.settings.getValue('SMS_GATEWAY_PASSWORD', '')).trim();



    const username =

      usernameFromDb ||

      (this.config.get<string>('HUAWEI_MODEM_USERNAME') ?? '').trim() ||

      'admin';



    const password =

      passwordFromDb || (this.config.get<string>('HUAWEI_MODEM_PASSWORD') ?? '').trim();



    return { username, password };

  }



  private normalizeModemUrl(value: string): string {

    const trimmed = value.trim().replace(/\/+$/, '');

    if (!/^https?:\/\//i.test(trimmed)) {

      return `http://${trimmed}`;

    }

    return trimmed;

  }



  private async fetchSessionTokens(modemUrl: string): Promise<ModemSessionTokens> {

    const initial = await this.requestSesTokInfo(modemUrl);

    let sessionCookie = initial.sesInfo;



    this.logDebugBlock('SesTokInfo (initial) Cookie / SesInfo', sessionCookie);

    this.logDebugBlock('SesTokInfo (initial) TokInfo', initial.tokInfo);



    const loginState = await this.resolveModemLoginState(modemUrl, sessionCookie);



    if (loginState.required) {

      const credentials = await this.resolveModemCredentials();

      if (!credentials.password) {

        throw new Error(

          'Модем Huawei требует авторизацию (код 100003). Укажите SMS_GATEWAY_USERNAME и SMS_GATEWAY_PASSWORD в настройках SMS или переменные HUAWEI_MODEM_USERNAME / HUAWEI_MODEM_PASSWORD'

        );

      }



      this.logger.debug(

        `Huawei HiLink: выполняется вход под пользователем ${credentials.username}`

      );



      const loggedIn = await this.loginToModem(

        modemUrl,

        sessionCookie,

        initial.tokInfo,

        credentials,

        loginState.passwordType

      );

      sessionCookie = loggedIn.sesInfo;



      this.logDebugBlock('Huawei login: новый Cookie / SesInfo', sessionCookie);

      this.logDebugBlock('Huawei login: __RequestVerificationToken', loggedIn.tokInfo);

    }



    const refreshed = await this.requestSesTokInfo(modemUrl, sessionCookie);



    this.logDebugBlock('SesTokInfo (refreshed) Cookie / SesInfo', refreshed.sesInfo);

    this.logDebugBlock('SesTokInfo (refreshed) TokInfo for POST', refreshed.tokInfo);



    return {

      sesInfo: refreshed.sesInfo,

      tokInfo: refreshed.tokInfo

    };

  }



  private async resolveModemLoginState(

    modemUrl: string,

    sessionCookie: string

  ): Promise<{ required: boolean; passwordType: string }> {

    const loginDisabled = await this.isModemLoginDisabled(modemUrl, sessionCookie);

    if (loginDisabled) {

      return { required: false, passwordType: '4' };

    }



    const { loggedIn, passwordType } = await this.fetchLoginState(modemUrl, sessionCookie);

    return { required: !loggedIn, passwordType };

  }



  private async isModemLoginDisabled(modemUrl: string, sessionCookie: string): Promise<boolean> {

    try {

      const response = await this.fetchWithTimeout(`${modemUrl}/config/global/config.xml`, {

        method: 'GET',

        headers: {

          Cookie: sessionCookie,

          Accept: '*/*',

          'X-Requested-With': 'XMLHttpRequest'

        }

      });



      if (!response.ok) {

        return false;

      }



      const configXml = await response.text();

      const loginFlag = this.extractXmlValue(configXml, 'login');

      return loginFlag === '0';

    } catch {

      return false;

    }

  }



  private async fetchLoginState(

    modemUrl: string,

    sessionCookie: string

  ): Promise<{ loggedIn: boolean; passwordType: string; state: string | null }> {

    const response = await this.fetchWithTimeout(`${modemUrl}/api/user/state-login`, {

      method: 'GET',

      headers: {

        Cookie: sessionCookie,

        Accept: '*/*',

        'X-Requested-With': 'XMLHttpRequest'

      }

    });



    if (!response.ok) {

      throw new Error(`state-login HTTP ${response.status}`);

    }



    const xml = await response.text();

    this.logger.debug(`state-login raw XML:\n${this.formatMultilineForLog(xml, 120, false)}`);



    const state = this.extractXmlValue(xml, 'State');

    const passwordType = this.extractXmlValue(xml, 'password_type') ?? '4';

    const loggedIn = state === '0';



    this.logger.debug(

      `state-login: State=${state ?? 'unknown'}, loggedIn=${loggedIn}, password_type=${passwordType}`

    );



    return { loggedIn, passwordType, state };

  }



  private async loginToModem(

    modemUrl: string,

    sessionCookie: string,

    verificationToken: string,

    credentials: ModemCredentials,

    passwordType: string

  ): Promise<ModemSessionTokens> {

    const encodedPassword = this.encodeHuaweiPassword(

      credentials.username,

      credentials.password,

      verificationToken

    );



    const body = [

      '<?xml version="1.0" encoding="UTF-8"?>',

      '<request>',

      `    <Username>${this.escapeXml(credentials.username)}</Username>`,

      `    <Password>${encodedPassword}</Password>`,

      `    <password_type>${this.escapeXml(passwordType)}</password_type>`,

      '</request>'

    ].join('\n');



    this.logDebugBlock('Huawei login POST Cookie', sessionCookie);

    this.logDebugBlock('Huawei login POST __RequestVerificationToken', verificationToken);

    console.log('[SmsService] Huawei login POST body:\n' + body);



    const response = await this.fetchWithTimeout(`${modemUrl}/api/user/login`, {

      method: 'POST',

      headers: {

        Cookie: sessionCookie,

        __RequestVerificationToken: verificationToken,

        'Content-Type': 'text/xml; charset=UTF-8',

        Accept: '*/*',

        'X-Requested-With': 'XMLHttpRequest',

        Origin: modemUrl,

        Referer: `${modemUrl}/html/index.html`

      },

      body

    });



    const responseText = await response.text();

    this.logger.debug(`login response:\n${this.formatMultilineForLog(responseText, 120, false)}`);



    const errorCode = this.extractXmlValue(responseText, 'code');

    if (errorCode && errorCode !== '0') {

      const errorMessage = this.extractXmlValue(responseText, 'message');

      throw new Error(

        errorMessage?.trim() || `login: код ошибки модема ${errorCode}`

      );

    }



    if (!/OK/i.test(responseText) && !response.ok) {

      throw new Error(`login HTTP ${response.status}: ${responseText.slice(0, 200).trim() || 'пустой ответ'}`);

    }



    const newCookie =

      this.extractSessionCookieFromHeaders(response) ??

      this.normalizeSesInfoCookie(this.extractXmlValue(responseText, 'SesInfo') ?? sessionCookie);



    const newToken =

      this.extractVerificationTokenFromHeaders(response) ?? verificationToken;



    return {

      sesInfo: newCookie,

      tokInfo: newToken

    };

  }



  private encodeHuaweiPassword(username: string, rawPassword: string, token: string): string {

    const sha256Hex = (value: string) =>

      createHash('sha256').update(value, 'utf8').digest('hex');



    const passwordHashB64 = Buffer.from(sha256Hex(rawPassword), 'utf8').toString('base64');

    const combined = username + passwordHashB64 + token;

    return Buffer.from(sha256Hex(combined), 'utf8').toString('base64');

  }



  private async requestSesTokInfo(modemUrl: string, sessionCookie?: string): Promise<ModemSessionTokens> {

    const headers: Record<string, string> = {

      Accept: '*/*',

      'X-Requested-With': 'XMLHttpRequest'

    };

    if (sessionCookie) {

      headers.Cookie = sessionCookie;

    }



    const response = await this.fetchWithTimeout(`${modemUrl}/api/webserver/SesTokInfo`, {

      method: 'GET',

      headers

    });



    if (!response.ok) {

      throw new Error(`SesTokInfo HTTP ${response.status}`);

    }



    const xml = await response.text();

    this.logger.debug(

      `SesTokInfo raw XML (${sessionCookie ? 'with cookie' : 'initial'}):\n${this.formatMultilineForLog(xml, 120, false)}`

    );

    this.logResponseHeaderTokens(response, 'SesTokInfo');



    const sesInfoRaw =

      this.extractSessionCookieFromHeaders(response) ??

      this.extractXmlValue(xml, 'SesInfo');



    const tokInfoRaw =

      this.extractVerificationTokenFromHeaders(response) ??

      this.extractXmlValue(xml, 'TokInfo');



    if (!sesInfoRaw || !tokInfoRaw) {

      throw new Error('Не удалось извлечь SesInfo/TokInfo из ответа модема');

    }



    const errorCode = this.extractXmlValue(xml, 'code');

    if (errorCode && errorCode !== '0') {

      const errorMessage = this.extractXmlValue(xml, 'message');

      throw new Error(errorMessage?.trim() || `SesTokInfo: код ошибки модема ${errorCode}`);

    }



    return {

      sesInfo: this.normalizeSesInfoCookie(sesInfoRaw),

      tokInfo: this.normalizeVerificationToken(tokInfoRaw)

    };

  }



  private extractSessionCookieFromHeaders(response: Response): string | null {

    const setCookies = this.readSetCookieHeaders(response);

    for (const entry of setCookies) {

      const match = entry.match(/SessionID=([^;]+)/i);

      if (match?.[1]) {

        return `SessionID=${this.sanitizeTokenValue(match[1])}`;

      }

    }

    return null;

  }



  private extractVerificationTokenFromHeaders(response: Response): string | null {

    const headerToken = response.headers.get('__RequestVerificationToken');

    if (!headerToken) {

      return null;

    }



    const tokens = headerToken

      .split('#')

      .map((part) => this.sanitizeTokenValue(part))

      .filter(Boolean);



    return tokens[0] ?? null;

  }



  private readSetCookieHeaders(response: Response): string[] {

    if (typeof response.headers.getSetCookie === 'function') {

      return response.headers.getSetCookie();

    }



    const single = response.headers.get('set-cookie');

    return single ? [single] : [];

  }



  private logResponseHeaderTokens(response: Response, label: string) {

    const verificationHeader = response.headers.get('__RequestVerificationToken');

    if (verificationHeader) {

      this.logDebugBlock(`${label} response header __RequestVerificationToken`, verificationHeader);

    }



    const setCookies = this.readSetCookieHeaders(response);

    if (setCookies.length) {

      this.logDebugBlock(`${label} response Set-Cookie`, setCookies.join('\n'));

    }

  }



  private normalizeSesInfoCookie(sesInfo: string): string {

    const compact = this.sanitizeTokenValue(sesInfo);

    if (!compact) {

      throw new Error('SesInfo пустой');

    }



    // XML returns <SesInfo>SessionID=YOUR_TOKEN</SesInfo> — pass EXACTLY this string as Cookie.

    if (/^SessionID=/i.test(compact)) {

      return compact;

    }



    if (compact.includes('=')) {

      return compact;

    }



    return `SessionID=${compact}`;

  }



  private normalizeVerificationToken(tokInfo: string): string {

    return this.sanitizeTokenValue(tokInfo);

  }



  private sanitizeTokenValue(value: string): string {

    return value.replace(/\s+/g, '').trim();

  }



  private async postSms(

    modemUrl: string,

    tokens: ModemSessionTokens,

    phoneNumber: string,

    messageText: string

  ): Promise<void> {

    const phone = this.formatPhoneForModem(phoneNumber);

    const content = messageText.trim();

    const body = [

      '<?xml version="1.0" encoding="UTF-8"?>',

      '<request>',

      '    <Index>-1</Index>',

      '    <Phones>',

      `        <Phone>${this.escapeXml(phone)}</Phone>`,

      '    </Phones>',

      '    <Sca></Sca>',

      `    <Content>${this.escapeXml(content)}</Content>`,

      '    <Length>-1</Length>',

      '    <Reserved>1</Reserved>',

      '    <Date>-1</Date>',

      '</request>'

    ].join('\n');



    const cookieHeader = tokens.sesInfo;

    const verificationToken = tokens.tokInfo;



    this.logSendSmsDebug(modemUrl, cookieHeader, verificationToken, body);



    const response = await this.fetchWithTimeout(`${modemUrl}/api/sms/send-sms`, {

      method: 'POST',

      headers: {

        Cookie: cookieHeader,

        __RequestVerificationToken: verificationToken,

        'Content-Type': 'text/xml; charset=UTF-8',

        Accept: '*/*',

        'X-Requested-With': 'XMLHttpRequest',

        Origin: modemUrl,

        Referer: `${modemUrl}/html/smsinbox.html`

      },

      body

    });



    const responseText = await response.text();

    this.logger.debug(`send-sms response:\n${this.formatMultilineForLog(responseText, 120, false)}`);



    if (!response.ok) {

      throw new Error(`send-sms HTTP ${response.status}: ${responseText.slice(0, 200).trim() || 'пустой ответ'}`);

    }



    const errorCode = this.extractXmlValue(responseText, 'code');

    if (errorCode && errorCode !== '0') {

      const errorMessage = this.extractXmlValue(responseText, 'message');

      throw new Error(errorMessage?.trim() || `send-sms: код ошибки модема ${errorCode}`);

    }



    if (/<error>/i.test(responseText)) {

      throw new Error('Модем вернул ошибку при отправке SMS');

    }



    if (!/OK/i.test(responseText) && !/<response>/i.test(responseText)) {

      throw new Error(`Неожиданный ответ модема: ${responseText.slice(0, 200).trim() || 'пустой ответ'}`);

    }

  }



  private logSendSmsDebug(modemUrl: string, cookie: string, token: string, body: string) {

    const cookieMeta = `[length=${cookie.length}, hasWhitespace=${/\s/.test(cookie)}]`;

    const tokenMeta = `[length=${token.length}, hasWhitespace=${/\s/.test(token)}]`;



    const lines = [

      '[SmsService] Huawei send-sms debug',

      `URL: ${modemUrl}/api/sms/send-sms`,

      `Cookie (SesInfo, used as-is) ${cookieMeta}:`,

      this.formatTokenForLog(cookie),

      `__RequestVerificationToken (TokInfo) ${tokenMeta}:`,

      this.formatTokenForLog(token),

      'XML body:',

      body

    ];



    const output = lines.join('\n');

    console.log(output);

    this.logger.debug(output);

  }



  private logDebugBlock(label: string, value: string) {

    const meta = `[length=${value.length}, hasWhitespace=${/\s/.test(value)}]`;

    const output = `[SmsService] ${label} ${meta}:\n${this.formatTokenForLog(value)}`;

    console.log(output);

    this.logger.debug(output);

  }



  private formatTokenForLog(value: string, chunkSize = this.logTokenChunkSize): string {

    const normalized = this.sanitizeTokenValue(value);

    return this.formatMultilineForLog(normalized, chunkSize, false);

  }



  private formatMultilineForLog(value: string, chunkSize: number, trimValue: boolean): string {

    const source = trimValue ? value.trim() : value;

    if (!source) {

      return '(empty)';

    }



    const chunks: string[] = [];

    for (let index = 0; index < source.length; index += chunkSize) {

      chunks.push(source.slice(index, index + chunkSize));

    }

    return chunks.join('\n');

  }



  private async fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {

    const controller = new AbortController();

    const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs);

    try {

      return await fetch(url, { ...init, signal: controller.signal });

    } catch (error) {

      if (error instanceof Error && error.name === 'AbortError') {

        throw new Error(`Таймаут запроса к модему (${this.requestTimeoutMs / 1000} с)`);

      }

      throw error;

    } finally {

      clearTimeout(timer);

    }

  }



  private extractXmlValue(xml: string, tag: string): string | null {

    const cdataMatch = xml.match(new RegExp(`<${tag}><!\\[CDATA\\[(.*?)\\]\\]><\\/${tag}>`, 's'));

    if (cdataMatch?.[1]) {

      return cdataMatch[1].trim();

    }



    const plainMatch = xml.match(new RegExp(`<${tag}>(.*?)<\\/${tag}>`, 's'));

    return plainMatch?.[1]?.trim() ?? null;

  }



  private formatPhoneForModem(phoneNumber: string): string {

    const digits = phoneNumber.replace(/\D/g, '');

    if (digits.length === 11 && digits.startsWith('8')) {

      return `+7${digits.slice(1)}`;

    }

    if (digits.length === 10) {

      return `+7${digits}`;

    }

    if (digits.startsWith('7')) {

      return `+${digits}`;

    }

    return phoneNumber.startsWith('+') ? phoneNumber : `+${digits}`;

  }



  private escapeXml(value: string): string {

    return value

      .replace(/&/g, '&amp;')

      .replace(/</g, '&lt;')

      .replace(/>/g, '&gt;')

      .replace(/"/g, '&quot;')

      .replace(/'/g, '&apos;');

  }



  private formatError(error: unknown, modemUrl: string): string {

    if (!(error instanceof Error)) {

      return 'Неизвестная ошибка';

    }



    const parts = [error.message?.trim()].filter(Boolean);

    const cause = (error as Error & { cause?: unknown }).cause;

    if (cause instanceof Error && cause.message.trim()) {

      parts.push(cause.message.trim());

    } else if (typeof cause === 'object' && cause !== null && 'code' in cause) {

      const code = String((cause as { code?: string }).code ?? '');

      if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EHOSTUNREACH') {

        parts.push(

          `Не удалось подключиться к ${modemUrl || 'модему'}. Проверьте IP в настройках SMS_GATEWAY_URL и доступность модема из контейнера sso-core`

        );

      } else if (code) {

        parts.push(code);

      }

    }



    return parts.join('. ') || 'Неизвестная ошибка';

  }



  private maskPhone(value: string): string {

    const digits = value.replace(/\D/g, '');

    return `***${digits.slice(-4)}`;

  }

}


