import { SimpleLocalClient, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot } from '../../core/index.js';
import type { IOllamaConfig } from './ollama.types.js';
import { ollamaDefaultName, ollamaProfile } from './ollama.types.js';

export class OllamaClient extends SimpleLocalClient<IOllamaConfig> {
  private currentOllamaSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IOllamaConfig) {
    super(ollamaProfile, configArg);
  }

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

    if (this.canUseNativeHttp()) {
      try {
        this.currentOllamaSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentOllamaSnapshot = this.offlineOllamaSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentOllamaSnapshot);
    }

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

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const tags = await this.getJson<Record<string, unknown>>('api/tags');
    const ps = await this.optionalJson<Record<string, unknown>>('api/ps');
    const psRecord = recordValue(ps);
    const models = arrayValue(tags.models);
    const runningModels = arrayValue(psRecord?.models);
    const endpoint = this.endpoint();
    const entities: ISimpleLocalEntitySnapshot[] = [
      {
        id: 'models',
        name: 'Models',
        platform: 'sensor',
        state: models.length,
        available: true,
        attributes: {
          model_names: models.map((modelArg) => stringValue(modelArg.name ?? modelArg.model)).filter((valueArg): valueArg is string => Boolean(valueArg)),
        },
      },
      {
        id: 'running_models',
        name: 'Running models',
        platform: 'sensor',
        state: runningModels.length,
        available: true,
        attributes: {
          model_names: runningModels.map((modelArg) => stringValue(modelArg.name ?? modelArg.model)).filter((valueArg): valueArg is string => Boolean(valueArg)),
          error: psRecord?.error,
        },
      },
    ];

    return {
      device: {
        id: this.configArg.uniqueId || `${endpoint.host}:${endpoint.port || 11434}${endpoint.path}`,
        name: this.configArg.name || ollamaDefaultName,
        manufacturer: ollamaProfile.manufacturer,
        model: ollamaProfile.model,
        host: endpoint.host,
        port: endpoint.port,
        protocol: endpoint.protocol,
        configurationUrl: this.baseUrl(),
        attributes: this.configArg.metadata,
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: { tags, ps },
      capabilities: {
        localRead: true,
        localControl: false,
        liveControlRequiresExecutor: false,
      },
    };
  }

  private async optionalJson<TValue>(pathArg: string): Promise<TValue | undefined | { error: string }> {
    try {
      return await this.getJson<TValue>(pathArg);
    } catch (errorArg) {
      return { error: errorMessage(errorArg) };
    }
  }

  private async getJson<TValue>(pathArg: string): Promise<TValue> {
    const response = await this.fetchWithTimeout(new URL(`${this.baseUrl()}/${pathArg.replace(/^\/+/, '')}`), { method: 'GET', headers: this.headers() });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`Ollama ${pathArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    return JSON.parse(text) as TValue;
  }

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

  private headers(): Record<string, string> {
    const headers: Record<string, string> = {
      accept: 'application/json',
      'content-type': 'application/json',
    };
    const apiKey = stringValue(this.configArg.apiKey) || stringValue(this.configArg.api_key) || stringValue(this.configArg.token);
    if (apiKey) {
      headers.authorization = `Bearer ${apiKey}`;
    }
    return headers;
  }

  private canUseNativeHttp(): boolean {
    return Boolean(this.configArg.url || this.configArg.host);
  }

  private baseUrl(): string {
    const endpoint = this.endpoint();
    return `${endpoint.protocol}://${endpoint.host}:${endpoint.port || 11434}${endpoint.path}`.replace(/\/+$/, '');
  }

  private endpoint(): { host: string; port: number; protocol: 'http' | 'https'; path: string } {
    const value = this.configArg.url || this.configArg.host;
    if (!value) {
      throw new Error('Ollama native HTTP requires config.url or config.host.');
    }
    const parsed = parseOllamaHost(value, this.configArg.useTls ? 'https' : 'http');
    if (this.configArg.port) {
      parsed.port = this.configArg.port;
    }
    return parsed;
  }

  private offlineOllamaSnapshot(errorArg: string): ISimpleLocalSnapshot {
    const endpoint = this.canUseNativeHttp() ? this.endpoint() : undefined;
    return {
      device: {
        id: this.configArg.uniqueId || (endpoint ? `${endpoint.host}:${endpoint.port}` : this.configArg.name || ollamaProfile.domain),
        name: this.configArg.name || ollamaDefaultName,
        manufacturer: ollamaProfile.manufacturer,
        model: ollamaProfile.model,
        host: endpoint?.host,
        port: endpoint?.port,
        protocol: endpoint?.protocol || 'http',
        configurationUrl: endpoint ? this.baseUrl() : undefined,
      },
      entities: [{ id: 'online', name: 'Online', platform: 'binary_sensor', state: false, available: true, attributes: { source: 'runtime' } }],
      online: false,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
    };
  }
}

const parseOllamaHost = (valueArg: string, defaultProtocolArg: 'http' | 'https'): { host: string; port: number; protocol: 'http' | 'https'; path: string } => {
  const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg);
  const url = new URL(hasProtocol ? valueArg : `${defaultProtocolArg}://${valueArg}`);
  const protocol = url.protocol === 'https:' ? 'https' : 'http';
  const defaultPort = hasProtocol ? protocol === 'https' ? 443 : 80 : 11434;
  return {
    host: url.hostname,
    port: url.port ? Number(url.port) : defaultPort,
    protocol,
    path: url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, ''),
  };
};

const arrayValue = (valueArg: unknown): Array<Record<string, unknown>> => Array.isArray(valueArg) ? valueArg.filter((itemArg): itemArg is Record<string, unknown> => Boolean(itemArg && typeof itemArg === 'object' && !Array.isArray(itemArg))) : [];

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

const stringValue = (valueArg: unknown): string | undefined => 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);
