import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallResult } from '../../core/index.js';
import { MutesyncMapper } from './mutesync.mapper.js';
import type { IMutesyncConfig, IMutesyncNativeRawData, IMutesyncStateResponse } from './mutesync.types.js';
import { mutesyncDefaultPort, mutesyncDefaultTimeoutMs, mutesyncProfile } from './mutesync.types.js';

export class MutesyncApiError extends Error {}
export class MutesyncApiConnectionError extends MutesyncApiError {}
export class MutesyncApiResponseError extends MutesyncApiError {}

export class MutesyncClient extends SimpleLocalClient<IMutesyncConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;
  private cachedToken?: string;

  constructor(private readonly configArg: IMutesyncConfig) {
    super(mutesyncProfile, configArg);
  }

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

    try {
      const rawData = await this.getRawData();
      this.liveSnapshot = MutesyncMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'http' });
    } catch (errorArg) {
      this.liveSnapshot = MutesyncMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
    }
    return clone(this.liveSnapshot);
  }

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

  public async getRawData(): Promise<IMutesyncNativeRawData> {
    if (!this.hasEndpoint()) {
      throw new MutesyncApiConnectionError('mutesync local HTTP polling requires config.host or config.url.');
    }
    const token = await this.authToken();
    const state = await this.requestState(token);
    if (state.muted === undefined || state.muted === null || state.in_meeting === undefined || state.in_meeting === null) {
      throw new MutesyncApiResponseError('mutesync /state returned an invalid response: muted and in_meeting are required.');
    }
    return { state, token, fetchedAt: new Date().toISOString() };
  }

  private async authToken(): Promise<string> {
    if (this.configArg.token) {
      return this.configArg.token;
    }
    if (this.cachedToken) {
      return this.cachedToken;
    }
    const response = await this.requestJson<{ token?: string }>('authenticate');
    if (!response.token) {
      throw new MutesyncApiResponseError('mutesync /authenticate did not return a token.');
    }
    this.cachedToken = response.token;
    return response.token;
  }

  private async requestState(tokenArg: string): Promise<IMutesyncStateResponse> {
    const response = await this.requestJson<{ data?: IMutesyncStateResponse }>('state', {
      authorization: `Token ${tokenArg}`,
      'x-mutesync-api-version': '1',
    });
    if (!response.data || typeof response.data !== 'object') {
      throw new MutesyncApiResponseError('mutesync /state did not return a data object.');
    }
    return response.data;
  }

  private async requestJson<TValue>(pathArg: string, headersArg: Record<string, string> = {}): Promise<TValue> {
    const response = await this.fetch(pathArg, headersArg);
    const text = await response.text();
    if (!response.ok) {
      throw new MutesyncApiConnectionError(`mutesync ${pathArg} request failed with ${response.status}: ${text.slice(0, 160)}`);
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new MutesyncApiResponseError(`mutesync ${pathArg} response was not JSON: ${errorMessage(errorArg)}`);
    }
  }

  private async fetch(pathArg: string, headersArg: Record<string, string>): Promise<Response> {
    const timeoutMs = this.configArg.timeoutMs || mutesyncDefaultTimeoutMs;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      return await globalThis.fetch(this.url(pathArg), {
        method: 'GET',
        headers: { accept: 'application/json', ...headersArg },
        signal: controller.signal,
      });
    } finally {
      clearTimeout(timer);
    }
  }

  private url(pathArg: string): string {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new MutesyncApiConnectionError('mutesync local HTTP polling requires config.host or config.url.');
    }
    return new URL(pathArg.replace(/^\/+/, ''), `${endpoint.origin}/`).toString();
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private endpoint(): URL | undefined {
    const configured = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!configured) {
      return undefined;
    }
    const withProtocol = /^https?:\/\//i.test(configured) ? configured : `${this.configArg.useTls ? 'https' : 'http'}://${configured}`;
    try {
      const url = new URL(withProtocol);
      if (!url.port && this.configArg.port) {
        url.port = String(this.configArg.port);
      } else if (!url.port) {
        url.port = String(mutesyncDefaultPort);
      }
      return url;
    } catch (errorArg) {
      throw new MutesyncApiConnectionError(`Invalid mutesync endpoint ${configured}: ${errorMessage(errorArg)}`);
    }
  }
}

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

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

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