import { DelugeMapper, endpointFromConfig } from './deluge.mapper.js';
import type { IDelugeCommandRequest, IDelugeConfig, IDelugeRawData, IDelugeSnapshot, IDelugeWebUpdateUiResponse } from './deluge.types.js';
import { delugeDefaultTimeoutMs } from './deluge.types.js';

const delugeTorrentKeys = [
  'name',
  'state',
  'paused',
  'progress',
  'download_payload_rate',
  'upload_payload_rate',
  'download_rate',
  'upload_rate',
  'total_size',
  'total_done',
  'ratio',
  'eta',
  'num_seeds',
  'num_peers',
  'tracker_host',
  'save_path',
];

const delugeSessionStatusKeys = ['download_rate', 'upload_rate', 'dht_download_rate', 'dht_upload_rate'];

export class DelugeHttpError extends Error {
  constructor(public readonly status: number, messageArg: string) {
    super(messageArg);
    this.name = 'DelugeHttpError';
  }
}

export class DelugeClient {
  private snapshot?: IDelugeSnapshot;
  private cookie?: string;
  private rpcId = 0;

  constructor(private readonly config: IDelugeConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<IDelugeSnapshot> {
    if (!forceRefreshArg && this.snapshot) {
      return this.snapshot;
    }
    if (!forceRefreshArg && this.config.snapshot) {
      this.snapshot = this.cloneSnapshot(this.config.snapshot);
      return this.snapshot;
    }
    if (this.config.client?.getSnapshot) {
      this.snapshot = this.snapshotFromUnknown(await this.config.client.getSnapshot(), 'client');
      return this.snapshot;
    }
    if (this.config.client?.getRawData) {
      this.snapshot = DelugeMapper.toSnapshot({ config: this.config, rawData: await this.config.client.getRawData(), online: true, source: 'client' });
      return this.snapshot;
    }
    if (this.config.rawData) {
      this.snapshot = DelugeMapper.toSnapshot({ config: this.config, rawData: this.config.rawData, online: this.config.connected ?? true, source: 'manual' });
      return this.snapshot;
    }
    if (this.hasLiveReadTarget()) {
      try {
        this.snapshot = await this.fetchSnapshot();
        return this.snapshot;
      } catch (errorArg) {
        this.snapshot = this.snapshotFromConfig(false, errorArg instanceof Error ? errorArg.message : String(errorArg));
        return this.snapshot;
      }
    }
    this.snapshot = this.snapshotFromConfig(this.config.connected ?? false);
    return this.snapshot;
  }

  public async validateConnection(): Promise<IDelugeSnapshot> {
    if (this.config.snapshot) {
      return this.cloneSnapshot(this.config.snapshot);
    }
    if (this.config.rawData) {
      return DelugeMapper.toSnapshot({ config: this.config, rawData: this.config.rawData, online: true, source: 'manual' });
    }
    if (this.config.client?.getSnapshot) {
      return this.snapshotFromUnknown(await this.config.client.getSnapshot(), 'client');
    }
    if (!this.hasLiveReadTarget()) {
      throw new Error('Deluge live validation requires config.host/config.url and a Deluge Web password, or a snapshot/client.');
    }
    return this.fetchSnapshot();
  }

  public async execute(commandArg: IDelugeCommandRequest): Promise<unknown> {
    if (commandArg.action === 'snapshot') {
      return this.getSnapshot(false);
    }
    if (commandArg.action === 'refresh') {
      return this.getSnapshot(true);
    }
    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(commandArg);
    }
    if (this.config.client?.execute) {
      return this.config.client.execute(commandArg);
    }
    if (this.config.client?.call) {
      return this.executeWithInjectedCall(commandArg);
    }
    if (!this.hasLiveReadTarget()) {
      throw new Error('Deluge controls require config.host/config.url and password, or an injected client/commandExecutor.');
    }
    return this.executeLive(commandArg);
  }

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

  private async fetchSnapshot(): Promise<IDelugeSnapshot> {
    await this.ensureAuthenticated();
    let connectError: string | undefined;
    if (this.hasDaemonCredentials()) {
      try {
        await this.ensureDaemonConnected();
      } catch (errorArg) {
        connectError = errorArg instanceof Error ? errorArg.message : String(errorArg);
      }
    }
    const webUi = await this.jsonRpc<IDelugeWebUpdateUiResponse>('web.update_ui', [delugeTorrentKeys, {}]);
    const rawData: Partial<IDelugeRawData> = {
      webUi,
      fetchedAt: new Date().toISOString(),
      errors: connectError ? { connect: connectError } : undefined,
    };
    if (webUi.connected) {
      rawData.sessionStatus = await this.jsonRpc<Record<string, unknown>>('core.get_session_status', [delugeSessionStatusKeys]).catch(() => undefined);
      rawData.sessionState = await this.jsonRpc<string[]>('core.get_session_state', []).catch(() => undefined);
    }
    return DelugeMapper.toSnapshot({ config: this.config, rawData, online: Boolean(webUi.connected), source: 'web', error: connectError });
  }

  private async executeLive(commandArg: IDelugeCommandRequest): Promise<unknown> {
    await this.ensureDaemonConnected();
    if (commandArg.action === 'pause' || commandArg.action === 'resume') {
      const torrentIds = await this.torrentIds(commandArg);
      if (!torrentIds.length) {
        return { ok: true, action: commandArg.action, torrentIds };
      }
      await this.jsonRpc(commandArg.action === 'pause' ? 'core.pause_torrent' : 'core.resume_torrent', [torrentIds]);
      this.snapshot = undefined;
      return { ok: true, action: commandArg.action, torrentIds };
    }
    if (commandArg.action === 'add_torrent') {
      const path = await this.torrentPath(commandArg);
      const result = await this.jsonRpc('web.add_torrents', [[{ path, options: commandArg.options || {} }]]);
      this.snapshot = undefined;
      return { ok: true, action: commandArg.action, path, result };
    }
    if (commandArg.action === 'remove_torrent') {
      const torrentIds = commandArg.torrentIds || [];
      if (!torrentIds.length) {
        throw new Error('Deluge remove_torrent requires data.torrent_id or data.torrent_ids.');
      }
      const results: unknown[] = [];
      for (const torrentId of torrentIds) {
        results.push(await this.jsonRpc('core.remove_torrent', [torrentId, commandArg.removeData === true]));
      }
      this.snapshot = undefined;
      return { ok: true, action: commandArg.action, torrentIds, results };
    }
    throw new Error(`Unsupported Deluge command: ${commandArg.action}`);
  }

  private async executeWithInjectedCall(commandArg: IDelugeCommandRequest): Promise<unknown> {
    const call = this.config.client?.call;
    if (!call) {
      throw new Error('Deluge injected client has no call method.');
    }
    if (commandArg.action === 'pause' || commandArg.action === 'resume') {
      const torrentIds = commandArg.torrentIds?.length ? commandArg.torrentIds : await this.injectedSessionState();
      return call(commandArg.action === 'pause' ? 'core.pause_torrent' : 'core.resume_torrent', torrentIds);
    }
    if (commandArg.action === 'add_torrent') {
      return call('web.add_torrents', [{ path: commandArg.magnet || commandArg.path || commandArg.url, options: commandArg.options || {} }]);
    }
    if (commandArg.action === 'remove_torrent') {
      const torrentIds = commandArg.torrentIds || [];
      if (!torrentIds.length) {
        throw new Error('Deluge remove_torrent requires data.torrent_id or data.torrent_ids.');
      }
      return Promise.all(torrentIds.map((torrentIdArg) => call('core.remove_torrent', torrentIdArg, commandArg.removeData === true)));
    }
    throw new Error(`Unsupported Deluge command: ${commandArg.action}`);
  }

  private async torrentIds(commandArg: IDelugeCommandRequest): Promise<string[]> {
    if (commandArg.torrentIds?.length) {
      return commandArg.torrentIds;
    }
    const sessionState = await this.jsonRpc<string[]>('core.get_session_state', []).catch(() => undefined);
    if (Array.isArray(sessionState)) {
      return sessionState.filter((itemArg) => typeof itemArg === 'string');
    }
    return (await this.getSnapshot()).torrents.map((torrentArg) => torrentArg.id);
  }

  private async injectedSessionState(): Promise<string[]> {
    const result = await this.config.client?.call?.('core.get_session_state');
    return Array.isArray(result) && result.every((itemArg) => typeof itemArg === 'string') ? result : [];
  }

  private async torrentPath(commandArg: IDelugeCommandRequest): Promise<string> {
    if (commandArg.magnet) {
      return commandArg.magnet;
    }
    if (commandArg.path) {
      return commandArg.path;
    }
    if (commandArg.url) {
      return this.jsonRpc<string>('web.download_torrent_from_url', [commandArg.url, commandArg.cookie || null]);
    }
    throw new Error('Deluge add_torrent requires data.magnet, data.url, or data.path.');
  }

  private async ensureDaemonConnected(): Promise<void> {
    await this.ensureAuthenticated();
    const connected = await this.jsonRpc<boolean>('web.connected', []).catch(() => false);
    if (connected) {
      return;
    }
    if (!this.hasDaemonCredentials()) {
      throw new Error('Deluge Web is not connected to a daemon; provide daemonHost/username/daemonPassword or connect it in Deluge Web.');
    }
    const hostId = await this.hostId();
    await this.jsonRpc('web.connect', [hostId]);
    const nowConnected = await this.jsonRpc<boolean>('web.connected', []).catch(() => false);
    if (!nowConnected) {
      throw new Error('Deluge Web did not connect to the configured daemon.');
    }
  }

  private async hostId(): Promise<string> {
    const endpoint = endpointFromConfig(this.config);
    const daemonHost = this.config.daemonHost || endpoint.host;
    const daemonPort = this.config.daemonPort || this.config.rpcPort || endpoint.rpcPort;
    const hosts = await this.jsonRpc<unknown[]>('web.get_hosts', []).catch(() => []);
    for (const host of hosts) {
      const hostRecord = Array.isArray(host) ? host : undefined;
      if (hostRecord && hostRecord.length >= 3 && hostRecord[1] === daemonHost && Number(hostRecord[2]) === daemonPort && typeof hostRecord[0] === 'string') {
        return hostRecord[0];
      }
    }
    const addResult = await this.jsonRpc<unknown[]>('web.add_host', [daemonHost, daemonPort, this.config.daemonUsername || this.config.username || '', this.config.daemonPassword || this.config.rpcPassword || this.config.password || '']);
    if (Array.isArray(addResult) && addResult[0] === true && typeof addResult[1] === 'string') {
      return addResult[1];
    }
    throw new Error(`Deluge Web could not add daemon host: ${Array.isArray(addResult) ? String(addResult[1]) : 'unknown error'}`);
  }

  private async ensureAuthenticated(): Promise<void> {
    if (this.cookie) {
      return;
    }
    const password = this.webPassword();
    if (!password) {
      throw new Error('Deluge Web JSON-RPC requires config.password or config.webPassword.');
    }
    const loggedIn = await this.jsonRpc<boolean>('auth.login', [password]);
    if (!loggedIn) {
      throw new Error('Deluge Web authentication failed.');
    }
  }

  private async jsonRpc<TValue = unknown>(methodArg: string, paramsArg: unknown[] = [], retriedArg = false): Promise<TValue> {
    const endpoint = endpointFromConfig(this.config);
    if (!endpoint.url) {
      throw new Error('Deluge Web JSON-RPC requires config.host or config.url.');
    }
    const headers = new Headers({ 'content-type': 'application/json' });
    if (this.cookie) {
      headers.set('cookie', this.cookie);
    }
    const response = await this.fetchWithTimeout(`${endpoint.url}/json`, {
      method: 'POST',
      headers,
      body: JSON.stringify({ method: methodArg, params: paramsArg, id: ++this.rpcId }),
    });
    const setCookie = response.headers.get('set-cookie');
    if (setCookie) {
      this.cookie = setCookie.split(';')[0];
    }
    if (!response.ok) {
      const text = await response.text().catch(() => '');
      throw new DelugeHttpError(response.status, `Deluge Web JSON-RPC ${methodArg} failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
    }
    const body = await response.json() as { result?: TValue; error?: { message?: string; code?: number } | null };
    if (body.error) {
      const message = body.error.message || `Deluge Web JSON-RPC error ${body.error.code ?? 'unknown'}`;
      if (!retriedArg && methodArg !== 'auth.login' && message.includes('Not authenticated')) {
        this.cookie = undefined;
        await this.ensureAuthenticated();
        return this.jsonRpc<TValue>(methodArg, paramsArg, true);
      }
      throw new Error(message);
    }
    return body.result as TValue;
  }

  private async fetchWithTimeout(urlArg: string, initArg: RequestInit): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.config.timeoutMs || delugeDefaultTimeoutMs);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private snapshotFromUnknown(valueArg: unknown, sourceArg: 'client'): IDelugeSnapshot {
    if (isDelugeSnapshot(valueArg)) {
      return this.cloneSnapshot(valueArg);
    }
    return DelugeMapper.toSnapshot({ config: this.config, rawData: valueArg && typeof valueArg === 'object' ? valueArg as Partial<IDelugeRawData> : {}, online: true, source: sourceArg });
  }

  private snapshotFromConfig(connectedArg: boolean, errorArg?: string): IDelugeSnapshot {
    return DelugeMapper.toSnapshot({ config: this.config, rawData: this.config.rawData || {}, online: connectedArg, source: this.config.snapshot ? 'snapshot' : 'manual', error: errorArg });
  }

  private hasLiveReadTarget(): boolean {
    const endpoint = endpointFromConfig(this.config);
    return Boolean(endpoint.url && this.webPassword());
  }

  private hasDaemonCredentials(): boolean {
    return Boolean((this.config.daemonHost || this.config.host || this.config.url) && (this.config.daemonUsername || this.config.username || this.config.daemonPassword || this.config.rpcPassword));
  }

  private webPassword(): string | undefined {
    return this.config.webPassword || this.config.password;
  }

  private cloneSnapshot(snapshotArg: IDelugeSnapshot): IDelugeSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IDelugeSnapshot;
  }
}

const isDelugeSnapshot = (valueArg: unknown): valueArg is IDelugeSnapshot => {
  return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'torrents' in valueArg && 'sensors' in valueArg);
};
