import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallResult } from '../../core/index.js';
import { PaperlessNgxMapper } from './paperless_ngx.mapper.js';
import type { IPaperlessNgxConfig, IPaperlessNgxRawApiData } from './paperless_ngx.types.js';
import { paperlessNgxDefaultTimeoutMs, paperlessNgxProfile } from './paperless_ngx.types.js';

interface IPaperlessEndpoint {
  baseUrl: string;
  host: string;
  port?: number;
  protocol: 'http' | 'https';
}

export class PaperlessNgxClient extends SimpleLocalClient<IPaperlessNgxConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly clientConfig: IPaperlessNgxConfig) {
    super(paperlessNgxProfile, clientConfig);
  }

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

    if (!forceRefreshArg && this.clientConfig.snapshot) {
      this.cachedSnapshot = PaperlessNgxMapper.toSnapshot({ config: this.clientConfig, source: 'snapshot', online: this.clientConfig.snapshot.online });
      return clone(this.cachedSnapshot);
    }

    if (this.endpoint()) {
      try {
        const rawData = await this.fetchRawData();
        this.cachedSnapshot = PaperlessNgxMapper.toSnapshot({ config: this.clientConfig, rawData, online: true, source: 'http' });
      } catch (errorArg) {
        this.cachedSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
    return clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const snapshot = await this.getSnapshot(Boolean(this.endpoint()));
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error || 'Paperless-ngx refresh requires a live URL with API token, injected client, snapshotProvider, snapshot, entities, or rawData.', data: snapshot };
  }

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

  private async fetchRawData(): Promise<IPaperlessNgxRawApiData> {
    if (!this.accessToken()) {
      throw new Error('Paperless-ngx local API snapshots require config.apiKey or config.token.');
    }

    const rawData: IPaperlessNgxRawApiData = {
      statistics: await this.requestJson('/api/statistics/'),
      fetchedAt: new Date().toISOString(),
    };

    try {
      rawData.status = await this.requestJson('/api/status/');
    } catch (errorArg) {
      rawData.statusError = errorMessage(errorArg);
    }

    try {
      rawData.remoteVersion = await this.requestJson('/api/remote_version/');
    } catch (errorArg) {
      rawData.remoteVersionError = errorMessage(errorArg);
    }

    return rawData;
  }

  private async requestJson<TValue = Record<string, unknown>>(pathArg: string): Promise<TValue> {
    const text = await this.requestText(pathArg);
    if (!text.trim()) {
      return {} as TValue;
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new Error(`Paperless-ngx JSON parse failed for ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async requestText(pathArg: string): Promise<string> {
    const endpoint = this.requireEndpoint();
    const url = new URL(pathArg.replace(/^\/+/, ''), `${endpoint.baseUrl}/`);
    const timeoutMs = this.clientConfig.timeoutMs || paperlessNgxDefaultTimeoutMs;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    const headers: Record<string, string> = {
      Accept: 'application/json; version=7',
      Authorization: `Token ${this.accessToken()}`,
    };

    try {
      const response = await globalThis.fetch(url, { headers, signal: controller.signal });
      const text = await response.text();
      if (response.status === 401) {
        throw new Error('Paperless-ngx API token is invalid, inactive, or deleted.');
      }
      if (response.status === 403) {
        throw new Error('Paperless-ngx API token is forbidden for this endpoint.');
      }
      if (!response.ok) {
        throw new Error(`Paperless-ngx request ${url.pathname} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return text;
    } finally {
      clearTimeout(timer);
    }
  }

  private accessToken(): string | undefined {
    return stringValue(this.clientConfig.apiKey) || stringValue(this.clientConfig.token);
  }

  private requireEndpoint(): IPaperlessEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Paperless-ngx local API requires config.url or config.host.');
    }
    return endpoint;
  }

  private endpoint(): IPaperlessEndpoint | undefined {
    const configuredUrl = stringValue(this.clientConfig.url);
    if (configuredUrl) {
      return normalizeEndpoint(configuredUrl, this.clientConfig.port, this.clientConfig.useTls);
    }
    const host = stringValue(this.clientConfig.host);
    if (!host) {
      return undefined;
    }
    return normalizeEndpoint(host, this.clientConfig.port, this.clientConfig.useTls);
  }

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return PaperlessNgxMapper.toSnapshot({ config: this.clientConfig, online: false, source: 'runtime', error: errorArg });
  }
}

const normalizeEndpoint = (valueArg: string, portArg: number | undefined, useTlsArg?: boolean): IPaperlessEndpoint | undefined => {
  try {
    const parsed = new URL(valueArg.includes('://') ? valueArg : `${useTlsArg === false ? 'http' : 'https'}://${valueArg}`);
    if (portArg) {
      parsed.port = String(portArg);
    }
    parsed.pathname = parsed.pathname.replace(/\/+$/, '');
    parsed.search = '';
    parsed.hash = '';
    return {
      baseUrl: parsed.toString().replace(/\/$/, ''),
      host: parsed.hostname,
      port: parsed.port ? Number(parsed.port) : undefined,
      protocol: parsed.protocol === 'http:' ? 'http' : 'https',
    };
  } catch {
    return undefined;
  }
};

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 => errorArg instanceof Error ? errorArg.message : String(errorArg);
