// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com

/**
 * UncloseAI Deno/TypeScript Library
 * OpenAI-compatible API client with streaming support
 * Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
 */

interface ModelInfo {
  id: string;
  endpoint: string;
  maxTokens: number;
}

interface Message {
  role: string;
  content: string;
}

interface ChatOptions {
  modelIdx?: number;
  maxTokens?: number;
  temperature?: number;
}

/**
 * UncloseAI Client class
 */
export class UncloseAI {
  public models: ModelInfo[] = [];
  public ttsEndpoints: string[] = [];
  private timeout: number;

  constructor(timeout = 30000) {
    this.timeout = timeout;
    this.discoverModels();
  }

  /**
   * Discover models from environment variables
   */
  private async discoverModels(): Promise<void> {
    console.log('Initializing UncloseAI client...');

    // Discover chat/code models
    for (let i = 1; i <= 9999; i++) {
      const endpoint = Deno.env.get(`MODEL_ENDPOINT_${i}`);
      if (!endpoint) break;

      console.log(`Endpoint ${i}: ${endpoint}`);
      await this.discoverModelsFromEndpoint(endpoint);
    }

    // Discover TTS endpoints
    for (let i = 1; i <= 9999; i++) {
      const endpoint = Deno.env.get(`TTS_ENDPOINT_${i}`);
      if (!endpoint) break;
      this.ttsEndpoints.push(endpoint);
    }

    console.log(`Discovered ${this.models.length} models, ${this.ttsEndpoints.length} TTS endpoints\n`);
  }

  /**
   * Discover models from a specific endpoint
   */
  private async discoverModelsFromEndpoint(endpoint: string): Promise<void> {
    try {
      const response = await fetch(`${endpoint}/models`, {
        signal: AbortSignal.timeout(10000)
      });
      const data = await response.json();

      if (data.data) {
        for (const model of data.data) {
          const modelId = model.id;

          // Skip modelperm-* entries
          if (modelId.startsWith('modelperm-')) continue;

          const maxTokens = model.max_model_len || 8192;
          this.models.push({ id: modelId, endpoint, maxTokens });
        }
      }
    } catch (_error) {
      // Silently skip failed endpoints
    }
  }

  /**
   * Non-streaming chat completion
   */
  async chat(messages: Message[], options: ChatOptions = {}): Promise<string> {
    const modelIdx = options.modelIdx ?? 0;
    const maxTokens = options.maxTokens ?? 100;
    const temperature = options.temperature ?? 0.7;

    if (modelIdx >= this.models.length) {
      throw new Error('Invalid model index');
    }

    const model = this.models[modelIdx];
    const url = `${model.endpoint}/chat/completions`;

    const request = {
      model: model.id,
      messages,
      stream: false,
      max_tokens: maxTokens,
      temperature
    };

    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(request),
      signal: AbortSignal.timeout(this.timeout)
    });

    const data = await response.json();
    return data.choices[0].message.content;
  }

  /**
   * Streaming chat completion - returns an async iterator
   */
  async *chatStream(messages: Message[], options: ChatOptions = {}): AsyncGenerator<string> {
    const modelIdx = options.modelIdx ?? 0;
    const maxTokens = options.maxTokens ?? 500;
    const temperature = options.temperature ?? 0.7;

    if (modelIdx >= this.models.length) {
      throw new Error('Invalid model index');
    }

    const model = this.models[modelIdx];
    const url = `${model.endpoint}/chat/completions`;

    const request = {
      model: model.id,
      messages,
      stream: true,
      max_tokens: maxTokens,
      temperature
    };

    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(request),
      signal: AbortSignal.timeout(this.timeout)
    });

    if (!response.body) {
      throw new Error('No response body');
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';

        for (const line of lines) {
          if (!line.startsWith('data: ')) continue;

          const data = line.slice(6).trim();
          if (data === '[DONE]') return;

          try {
            const json = JSON.parse(data);
            const content = json.choices?.[0]?.delta?.content;
            if (content) {
              yield content;
            }
          } catch {
            // Skip malformed JSON
          }
        }
      }
    } finally {
      reader.releaseLock();
    }
  }

  /**
   * Text-to-speech generation
   */
  async tts(text: string, voice = 'alloy', outputFile = '/tmp/speech.mp3'): Promise<boolean> {
    if (this.ttsEndpoints.length === 0) return false;

    const endpoint = this.ttsEndpoints[0];
    const url = `${endpoint}/audio/speech`;

    const request = {
      model: 'tts-1',
      voice,
      input: text
    };

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(request),
        signal: AbortSignal.timeout(this.timeout)
      });

      const audioData = await response.arrayBuffer();
      await Deno.writeFile(outputFile, new Uint8Array(audioData));
      return true;
    } catch {
      return false;
    }
  }
}

/**
 * Demo program showing library usage
 */
if (import.meta.main) {
  console.log('=== UncloseAI Deno Client (with Streaming) ===\n');

  // Initialize client
  const client = new UncloseAI();

  // Wait for discovery to complete
  await new Promise(resolve => setTimeout(resolve, 1000));

  if (client.models.length === 0) {
    console.log('ERROR: No models discovered');
    Deno.exit(1);
  }

  // Non-streaming chat example
  console.log('=== Non-Streaming Chat ===');
  console.log(`Model: ${client.models[0].id}`);

  try {
    const messages = [{ role: 'user', content: 'Explain quantum computing in one sentence' }];
    const response = await client.chat(messages);
    console.log(`Response: ${response}\n`);
  } catch (error) {
    console.log(`Error: ${error.message}\n`);
  }

  // Streaming chat example
  const modelIdx = client.models.length >= 2 ? 1 : 0;
  console.log('=== Streaming Chat ===');
  console.log(`Model: ${client.models[modelIdx].id}`);
  Deno.stdout.writeSync(new TextEncoder().encode('Response: '));

  try {
    const messages = [{ role: 'user', content: 'Write a hello world program in Deno TypeScript' }];
    for await (const content of client.chatStream(messages, { modelIdx })) {
      Deno.stdout.writeSync(new TextEncoder().encode(content));
    }
    console.log('\n');
  } catch (error) {
    console.log(`\nError: ${error.message}\n`);
  }

  // TTS example
  if (client.ttsEndpoints.length > 0) {
    console.log('=== TTS Speech Generation ===');
    console.log('Model: tts-1');

    if (await client.tts('Hello from UncloseAI Deno client!', 'alloy', '/tmp/speech.mp3')) {
      console.log('Audio saved to /tmp/speech.mp3');
    } else {
      console.log('TTS failed');
    }
  }

  console.log('\n=== Examples Complete ===');
}
