import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot, type TSimpleLocalRawData } from '../../core/index.js';
import { OctoprintMapper } from './octoprint.mapper.js';
import type { IOctoprintConfig, IOctoprintRawData } from './octoprint.types.js';
import { octoprintDefaultPath, octoprintDefaultPort, octoprintDefaultTimeoutMs, octoprintProfile } from './octoprint.types.js';

export class OctoprintApiError extends Error {}
export class OctoprintApiConnectionError extends OctoprintApiError {}
export class OctoprintApiAuthorizationError extends OctoprintApiError {}
export class OctoprintApiResponseError extends OctoprintApiError {}
export class OctoprintUnsupportedCommandError extends OctoprintApiError {}

export class OctoprintClient extends SimpleLocalClient<IOctoprintConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(configArg: IOctoprintConfig) {
    super(octoprintProfile, configArg);
    this.octoprintConfig = configArg;
  }

  private readonly octoprintConfig: IOctoprintConfig;

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

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

    if (this.octoprintConfig.client || this.octoprintConfig.snapshotProvider) {
      this.cachedSnapshot = await this.snapshotFromInjectedSource();
      return this.clone(this.cachedSnapshot);
    }

    if (!forceRefreshArg && this.hasManualData()) {
      this.cachedSnapshot = OctoprintMapper.toSnapshot({
        config: this.octoprintConfig,
        rawData: this.octoprintConfig.rawData ?? this.octoprintConfig.rawText,
        online: this.octoprintConfig.online ?? true,
        source: 'manual',
      });
      return this.clone(this.cachedSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        this.cachedSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.cachedSnapshot = this.buildOfflineSnapshot(this.errorMessage(errorArg));
      }
      return this.clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = this.buildOfflineSnapshot('No OctoPrint REST endpoint, client, snapshotProvider, snapshot, entities, or rawData is configured.');
    return this.clone(this.cachedSnapshot);
  }

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

  public async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const rawData: IOctoprintRawData = {
      job: await this.requestJson<Record<string, unknown>>('api/job'),
      fetchedAt: new Date().toISOString(),
      errors: {},
    };
    try {
      rawData.printer = await this.requestJson<Record<string, unknown>>('api/printer', { allowPrinterOffline: true });
    } catch (errorArg) {
      throw errorArg;
    }
    try {
      rawData.settings = await this.requestJson<Record<string, unknown>>('api/settings');
    } catch (errorArg) {
      rawData.errors!.settings = this.errorMessage(errorArg);
    }
    return OctoprintMapper.toSnapshot({ config: this.octoprintConfig, rawData, online: true, source: 'http' });
  }

  public override async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.octoprintConfig.client?.execute) {
      return this.normalizeServiceResult(await this.octoprintConfig.client.execute(requestArg));
    }
    if (this.octoprintConfig.commandExecutor) {
      return this.normalizeServiceResult(await this.octoprintConfig.commandExecutor.execute(requestArg));
    }
    if (requestArg.service === 'refresh') {
      return this.refresh();
    }
    if (!this.hasEndpoint()) {
      return {
        success: false,
        error: 'OctoPrint commands require a live REST endpoint, an injected client.execute(), or commandExecutor. Static snapshots/manual data are read-only.',
      };
    }

    try {
      const data = await this.executeLiveCommand(requestArg);
      this.cachedSnapshot = undefined;
      return { success: true, data };
    } catch (errorArg) {
      return { success: false, error: this.errorMessage(errorArg) };
    }
  }

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

  private async snapshotFromInjectedSource(): Promise<ISimpleLocalSnapshot> {
    const client = this.octoprintConfig.client;
    if (client?.getSnapshot) {
      const value = await client.getSnapshot();
      return this.snapshotFromValue(value, 'client');
    }
    if (client?.getRawData) {
      return this.snapshotFromValue(await client.getRawData(), 'client');
    }
    if (client?.getState) {
      return this.snapshotFromValue(await client.getState(), 'client');
    }
    if (this.octoprintConfig.snapshotProvider) {
      const value = await this.octoprintConfig.snapshotProvider();
      if (value !== undefined) {
        return this.snapshotFromValue(value, 'provider');
      }
    }
    return super.getSnapshot(true);
  }

  private snapshotFromValue(valueArg: ISimpleLocalSnapshot | TSimpleLocalRawData, sourceArg: ISimpleLocalSnapshot['source']): ISimpleLocalSnapshot {
    if (this.isSnapshot(valueArg)) {
      return OctoprintMapper.toSnapshot({ config: { ...this.octoprintConfig, snapshot: valueArg }, source: sourceArg, online: valueArg.online });
    }
    return OctoprintMapper.toSnapshot({ config: this.octoprintConfig, rawData: valueArg, online: true, source: sourceArg });
  }

  private async executeLiveCommand(requestArg: IServiceCallRequest): Promise<unknown> {
    if (requestArg.service === 'printer_connect' || requestArg.service === 'connect') {
      return this.postJson('api/connection', {
        command: 'connect',
        port: this.stringValue(requestArg.data?.port),
        baudrate: this.numberValue(requestArg.data?.baudrate),
        printerprofile: this.stringValue(requestArg.data?.profileName || requestArg.data?.printerProfile),
        save: this.booleanValue(requestArg.data?.save),
        autoconnect: this.booleanValue(requestArg.data?.autoConnect),
      });
    }
    if (requestArg.service === 'set_value' || requestArg.service === 'set_temperature') {
      return this.setTemperature(requestArg);
    }
    const action = OctoprintMapper.actionForService(requestArg);
    switch (action) {
      case 'pause_job':
        return this.postJson('api/job', { command: 'pause', action: 'pause' });
      case 'resume_job':
        return this.postJson('api/job', { command: 'pause', action: 'resume' });
      case 'stop_job':
        return this.postJson('api/job', { command: 'cancel' });
      case 'shutdown_system':
        return this.postJson('api/system/commands/core/shutdown');
      case 'reboot_system':
        return this.postJson('api/system/commands/core/reboot');
      case 'restart_octoprint':
        return this.postJson('api/system/commands/core/restart');
      default:
        throw new OctoprintUnsupportedCommandError(`Unsupported OctoPrint service: ${requestArg.service}`);
    }
  }

  private async setTemperature(requestArg: IServiceCallRequest): Promise<unknown> {
    const value = this.numberValue(requestArg.data?.value ?? requestArg.data?.temperature ?? requestArg.data?.target);
    if (value === undefined) {
      throw new OctoprintUnsupportedCommandError('OctoPrint set_value requires data.value, data.temperature, or data.target.');
    }
    const tool = this.stringValue(requestArg.data?.tool || requestArg.data?.targetTool) || this.toolFromEntityId(requestArg.target?.entityId);
    if (!tool) {
      throw new OctoprintUnsupportedCommandError('OctoPrint set_value requires data.tool or an entity id containing bed/toolN.');
    }
    if (tool === 'bed') {
      return this.postJson('api/printer/bed', { command: 'target', target: Math.round(value), offset: this.numberValue(requestArg.data?.offset) || 0 });
    }
    return this.postJson('api/printer/tool', { command: 'target', targets: { [tool]: Math.round(value) } });
  }

  private async requestJson<TValue>(pathArg: string, optionsArg: { allowPrinterOffline?: boolean } = {}): Promise<TValue> {
    const response = await this.request(pathArg, { method: 'GET' });
    if (response.status === 409 && optionsArg.allowPrinterOffline) {
      return null as TValue;
    }
    return this.responseJson<TValue>(response, pathArg);
  }

  private async postJson(pathArg: string, bodyArg?: Record<string, unknown>): Promise<unknown> {
    const response = await this.request(pathArg, {
      method: 'POST',
      body: bodyArg ? JSON.stringify(this.cleanPayload(bodyArg)) : undefined,
    });
    if (response.status === 204) {
      return { ok: true };
    }
    if (response.ok) {
      const text = await response.text();
      return text ? JSON.parse(text) as unknown : { ok: true };
    }
    return this.responseJson(response, pathArg);
  }

  private async request(pathArg: string, optionsArg: { method: 'GET' | 'POST'; body?: string }): Promise<Response> {
    const url = this.buildUrl(pathArg);
    const headers = this.headers(Boolean(optionsArg.body));
    try {
      return await globalThis.fetch(url, {
        method: optionsArg.method,
        headers,
        body: optionsArg.body,
        signal: AbortSignal.timeout(this.octoprintConfig.timeoutMs || octoprintDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new OctoprintApiConnectionError(`Connection to ${url} failed: ${this.errorMessage(errorArg)}`);
    }
  }

  private async responseJson<TValue>(responseArg: Response, pathArg: string): Promise<TValue> {
    const text = await responseArg.text();
    if (responseArg.status === 401 || responseArg.status === 403) {
      throw new OctoprintApiAuthorizationError('Please check your OctoPrint API key.');
    }
    if (!responseArg.ok) {
      throw new OctoprintApiResponseError(`OctoPrint endpoint ${pathArg} failed with HTTP ${responseArg.status}: ${text.slice(0, 160)}`);
    }
    if (!text) {
      return undefined as TValue;
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new OctoprintApiResponseError(`Unable to parse OctoPrint JSON from ${pathArg}: ${this.errorMessage(errorArg)}`);
    }
  }

  private buildUrl(pathArg: string): string {
    return new URL(pathArg.replace(/^\/+/, ''), this.baseUrl()).toString();
  }

  private baseUrl(): string {
    if (this.octoprintConfig.url) {
      const parsed = new URL(this.octoprintConfig.url);
      if (!parsed.pathname.endsWith('/')) {
        parsed.pathname = `${parsed.pathname}/`;
      }
      return parsed.toString();
    }
    const host = this.octoprintConfig.host;
    if (!host) {
      throw new OctoprintApiConnectionError('OctoPrint REST URL requires config.host or config.url.');
    }
    const useTls = Boolean(this.octoprintConfig.useTls || this.octoprintConfig.ssl);
    const port = this.octoprintConfig.port || octoprintDefaultPort;
    const path = this.octoprintConfig.path || octoprintDefaultPath;
    const normalizedPath = path.endsWith('/') ? path : `${path}/`;
    return `${useTls ? 'https' : 'http'}://${hostForUrl(host)}:${port}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`;
  }

  private headers(hasBodyArg: boolean): Record<string, string> {
    const headers: Record<string, string> = { accept: 'application/json' };
    if (hasBodyArg) {
      headers['content-type'] = 'application/json';
    }
    const apiKey = this.octoprintConfig.apiKey || this.octoprintConfig.token;
    if (apiKey) {
      headers['X-Api-Key'] = String(apiKey);
    }
    return headers;
  }

  private toolFromEntityId(entityIdArg: string | undefined): string | undefined {
    if (!entityIdArg) {
      return undefined;
    }
    if (/bed/i.test(entityIdArg)) {
      return 'bed';
    }
    return entityIdArg.match(/tool\d+/i)?.[0].toLowerCase();
  }

  private hasEndpoint(): boolean {
    return Boolean(this.octoprintConfig.url || this.octoprintConfig.host);
  }

  private hasManualData(): boolean {
    return Boolean(this.octoprintConfig.rawData !== undefined || this.octoprintConfig.rawText !== undefined || this.octoprintConfig.entities?.length);
  }

  private buildOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return OctoprintMapper.toSnapshot({ config: this.octoprintConfig, online: false, source: 'runtime', error: errorArg });
  }

  private normalizeServiceResult(valueArg: unknown): IServiceCallResult {
    if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
      return valueArg as IServiceCallResult;
    }
    return { success: true, data: valueArg };
  }

  private isSnapshot(valueArg: unknown): valueArg is ISimpleLocalSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg && 'online' in valueArg);
  }

  private cleanPayload(valueArg: Record<string, unknown>): Record<string, unknown> {
    return Object.fromEntries(Object.entries(valueArg).filter(([, value]) => value !== undefined));
  }

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

  private numberValue(valueArg: unknown): number | undefined {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg;
    }
    if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
      return Number(valueArg);
    }
    return undefined;
  }

  private booleanValue(valueArg: unknown): boolean | undefined {
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    if (typeof valueArg === 'number') {
      return valueArg !== 0;
    }
    if (typeof valueArg === 'string') {
      const normalized = valueArg.trim().toLowerCase();
      if (['true', 'yes', 'on', '1'].includes(normalized)) {
        return true;
      }
      if (['false', 'no', 'off', '0'].includes(normalized)) {
        return false;
      }
    }
    return undefined;
  }

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

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

const hostForUrl = (hostArg: string): string => hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
