import type { IServiceCallRequest, IServiceCallResult, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
import { MaryttsMapper } from './marytts.mapper.js';
import type { IMaryttsAudioResult, IMaryttsConfig, IMaryttsSpeakOptions, IMaryttsSpeakRequest, TMaryttsAudioFormat } from './marytts.types.js';
import {
  maryttsDefaultCodec,
  maryttsDefaultHost,
  maryttsDefaultInputType,
  maryttsDefaultLanguage,
  maryttsDefaultPort,
  maryttsDefaultTimeoutMs,
  maryttsDefaultVoice,
} from './marytts.types.js';

export class MaryttsClientError extends Error {
  constructor(messageArg: string) {
    super(messageArg);
    this.name = 'MaryttsClientError';
  }
}

export class MaryttsClient {
  private currentSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly config: IMaryttsConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.currentSnapshot) {
      return this.clone(this.currentSnapshot);
    }

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = MaryttsMapper.toSnapshot({ config: this.config, rawData: this.config.snapshot, online: this.config.snapshot.online, source: 'snapshot' });
      return this.clone(this.currentSnapshot);
    }

    if (this.config.client?.getSnapshot || this.config.client?.getState || this.config.client?.getRawData) {
      try {
        this.currentSnapshot = MaryttsMapper.toSnapshot({ config: this.config, rawData: await this.snapshotFromClient(), online: true, source: 'client' });
      } catch (errorArg) {
        this.currentSnapshot = MaryttsMapper.toStatusSnapshot(this.config, { online: false, source: 'runtime', error: this.errorMessage(errorArg) });
      }
      return this.clone(this.currentSnapshot);
    }

    if (this.config.snapshotProvider) {
      try {
        const snapshot = await this.config.snapshotProvider();
        if (snapshot !== undefined) {
          this.currentSnapshot = MaryttsMapper.toSnapshot({ config: this.config, rawData: snapshot, online: true, source: 'provider' });
          return this.clone(this.currentSnapshot);
        }
      } catch (errorArg) {
        this.currentSnapshot = MaryttsMapper.toStatusSnapshot(this.config, { online: false, source: 'runtime', error: this.errorMessage(errorArg) });
        return this.clone(this.currentSnapshot);
      }
    }

    const rawData = this.config.rawData ?? this.config.rawText;
    if (rawData !== undefined || this.config.entities) {
      this.currentSnapshot = MaryttsMapper.toSnapshot({ config: this.config, rawData: rawData as TSimpleLocalRawData | undefined, online: this.config.online ?? true, source: 'manual' });
      return this.clone(this.currentSnapshot);
    }

    if (this.hasConfiguredEndpoint()) {
      try {
        await this.probe();
        this.currentSnapshot = MaryttsMapper.toStatusSnapshot(this.config, { online: true, source: 'http' });
      } catch (errorArg) {
        this.currentSnapshot = MaryttsMapper.toStatusSnapshot(this.config, { online: false, source: 'runtime', error: this.errorMessage(errorArg) });
      }
      return this.clone(this.currentSnapshot);
    }

    this.currentSnapshot = MaryttsMapper.toStatusSnapshot(this.config, { online: this.config.online ?? false, source: 'manual' });
    return this.clone(this.currentSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    const snapshot = await this.getSnapshot(true);
    return { success: snapshot.online && !snapshot.error, error: snapshot.error, data: snapshot };
  }

  public async speak(messageArg: string, optionsArg: IMaryttsSpeakOptions = {}): Promise<IMaryttsAudioResult> {
    if (this.config.client?.speak) {
      return this.config.client.speak(messageArg, optionsArg);
    }

    const message = stringValue(messageArg);
    if (!message) {
      throw new MaryttsClientError('MaryTTS synthesis requires a non-empty message.');
    }

    const codec = stringValue(optionsArg.codec) || stringValue(this.config.codec) || maryttsDefaultCodec;
    const language = stringValue(optionsArg.language) || stringValue(optionsArg.locale) || stringValue(this.config.language) || stringValue(this.config.locale) || maryttsDefaultLanguage;
    const voice = stringValue(optionsArg.voice) || stringValue(this.config.voice) || maryttsDefaultVoice;
    const inputType = stringValue(optionsArg.inputType) || stringValue(this.config.inputType) || maryttsDefaultInputType;
    const effects = { ...this.config.effect, ...this.config.effects, ...optionsArg.effect, ...optionsArg.effects };
    const body = new URLSearchParams({
      INPUT_TEXT: message,
      OUTPUT_TYPE: 'AUDIO',
      INPUT_TYPE: inputType,
      LOCALE: language,
      AUDIO: codec,
      VOICE: voice,
    });

    for (const [effect, parameters] of Object.entries(effects)) {
      if (!effect) {
        continue;
      }
      body.set(`effect_${effect}_selected`, 'on');
      body.set(`effect_${effect}_parameters`, parameters);
    }

    const response = await globalThis.fetch(this.requestUrl('/process'), {
      method: 'POST',
      headers: {
        accept: 'audio/*, application/octet-stream;q=0.9, */*;q=0.8',
        'content-type': 'application/x-www-form-urlencoded',
      },
      body,
      signal: AbortSignal.timeout(this.timeoutMs()),
    });

    if (!response.ok) {
      throw new MaryttsClientError(`MaryTTS /process request failed with HTTP ${response.status}: ${(await response.text()).slice(0, 200)}`);
    }

    return {
      format: this.audioFormat(codec),
      codec,
      contentType: response.headers.get('content-type') || undefined,
      language,
      voice,
      effects,
      audio: Buffer.from(await response.arrayBuffer()),
    };
  }

  public async synthesize(requestArg: IMaryttsSpeakRequest): Promise<IMaryttsAudioResult> {
    if (this.config.client?.synthesize) {
      return this.config.client.synthesize(requestArg);
    }
    return this.speak(requestArg.message, requestArg);
  }

  public async execute(requestArg: IServiceCallRequest | IMaryttsSpeakRequest): Promise<IServiceCallResult> {
    if (isServiceCall(requestArg)) {
      if (requestArg.service === 'snapshot' || requestArg.service === 'status') {
        return { success: true, data: await this.getSnapshot() };
      }
      if (requestArg.service === 'refresh' || requestArg.service === 'reload') {
        return this.refresh();
      }
      if (!['get_tts_audio', 'say', 'speak'].includes(requestArg.service)) {
        return { success: false, error: `Unsupported MaryTTS service: ${requestArg.domain}.${requestArg.service}` };
      }
      try {
        return { success: true, data: await this.synthesize(this.speakRequestFromService(requestArg)) };
      } catch (errorArg) {
        return { success: false, error: this.errorMessage(errorArg) };
      }
    }

    try {
      return { success: true, data: await this.synthesize(requestArg) };
    } catch (errorArg) {
      return { success: false, error: this.errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await this.config.client?.destroy?.();
  }

  private async probe(): Promise<void> {
    const response = await globalThis.fetch(this.requestUrl('/'), {
      method: 'GET',
      signal: AbortSignal.timeout(Math.min(this.timeoutMs(), 5000)),
    });
    if (!response.ok) {
      throw new MaryttsClientError(`MaryTTS probe failed with HTTP ${response.status}: ${(await response.text()).slice(0, 160)}`);
    }
  }

  private async snapshotFromClient(): Promise<ISimpleLocalSnapshot | TSimpleLocalRawData> {
    const client = this.config.client;
    if (!client) {
      throw new MaryttsClientError('MaryTTS client is not configured.');
    }
    if (client.getSnapshot) {
      return client.getSnapshot();
    }
    if (client.getState) {
      return client.getState();
    }
    if (client.getRawData) {
      return client.getRawData();
    }
    throw new MaryttsClientError('MaryTTS client must expose getSnapshot(), getState(), or getRawData().');
  }

  private speakRequestFromService(requestArg: IServiceCallRequest): IMaryttsSpeakRequest {
    const data = requestArg.data || {};
    const options = objectValue(data.options);
    const message = stringValue(data.message) || stringValue(data.text) || stringValue(data.input);
    if (!message) {
      throw new MaryttsClientError('MaryTTS speak/get_tts_audio requires data.message.');
    }

    return {
      message,
      language: stringValue(data.language) || stringValue(options?.language),
      locale: stringValue(data.locale) || stringValue(options?.locale),
      voice: stringValue(data.voice) || stringValue(options?.voice),
      codec: stringValue(data.codec) || stringValue(options?.codec),
      inputType: stringValue(data.inputType) || stringValue(options?.inputType),
      effect: recordOfStrings(data.effect) || recordOfStrings(options?.effect),
      effects: recordOfStrings(data.effects) || recordOfStrings(options?.effects),
    };
  }

  private requestUrl(pathArg: string): string {
    const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
    if (this.config.url) {
      const configured = new URL(this.config.url);
      if (path === '/process' && configured.pathname.endsWith('/process')) {
        return configured.toString();
      }
      const basePath = configured.pathname.replace(/\/+$/g, '');
      return `${configured.protocol}//${configured.host}${basePath}${path}`;
    }

    const protocol = this.config.useTls || this.config.ssl ? 'https' : 'http';
    const host = this.config.host || maryttsDefaultHost;
    const port = this.config.port || maryttsDefaultPort;
    return `${protocol}://${host}:${port}${path}`;
  }

  private hasConfiguredEndpoint(): boolean {
    return Boolean(this.config.url || this.config.host || this.config.port || this.config.useTls || this.config.ssl);
  }

  private timeoutMs(): number {
    return this.config.timeoutMs || maryttsDefaultTimeoutMs;
  }

  private audioFormat(codecArg: string): TMaryttsAudioFormat {
    if (codecArg === 'WAVE_FILE') return 'wav';
    if (codecArg === 'AIFF_FILE') return 'aiff';
    if (codecArg === 'AU_FILE') return 'au';
    return codecArg.toLowerCase().replace(/_file$/i, '');
  }

  private clone<TValue>(valueArg: TValue): TValue {
    return JSON.parse(JSON.stringify(valueArg)) as TValue;
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }
}

const isServiceCall = (valueArg: IServiceCallRequest | IMaryttsSpeakRequest): valueArg is IServiceCallRequest => {
  return typeof (valueArg as IServiceCallRequest).service === 'string' && typeof (valueArg as IServiceCallRequest).domain === 'string';
};

const stringValue = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
};

const objectValue = (valueArg: unknown): Record<string, unknown> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
};

const recordOfStrings = (valueArg: unknown): Record<string, string> | undefined => {
  const value = objectValue(valueArg);
  if (!value) {
    return undefined;
  }
  const entries = Object.entries(value)
    .filter((entryArg): entryArg is [string, string] => typeof entryArg[1] === 'string')
    .filter(([keyArg]) => Boolean(keyArg));
  return entries.length ? Object.fromEntries(entries) : undefined;
};
