"use strict";

const childProcess = require("node:child_process");
const crypto = require("node:crypto");
const fs = require("node:fs");

class HoloLakeKnowledgeProvider {
  constructor(options = {}) {
    this.repositoryId = String(options.repositoryId || "");
    this.repositoryPath = String(options.repositoryPath || "");
    this.ref = "refs/heads/main";
    this.maxArchiveBytes = Math.max(
      1024 * 1024,
      Number(options.maxArchiveBytes || 128 * 1024 * 1024),
    );
    if (!/^[a-z0-9._-]+\/[a-z0-9._-]+$/.test(this.repositoryId)) {
      throw new Error("knowledge_repository_id_invalid");
    }
    if (!this.repositoryPath || !fs.existsSync(this.repositoryPath)) {
      throw new Error("knowledge_repository_unavailable");
    }
  }

  manifest() {
    const commit = this.git(["rev-parse", "--verify", `${this.ref}^{commit}`])
      .toString("utf8")
      .trim();
    if (!/^[0-9a-f]{40}$/.test(commit)) {
      throw new Error("knowledge_commit_invalid");
    }
    const committedAt = Number(
      this.git(["show", "-s", "--format=%ct", commit]).toString("utf8").trim(),
    );
    return {
      schema: "guanghu.hololake-knowledge-manifest/v1",
      repository: this.repositoryId,
      ref: this.ref,
      commit,
      committed_at: committedAt,
      archive_url: `/api/hololake/knowledge/archive?commit=${commit}`,
    };
  }

  archive(commit) {
    const manifest = this.manifest();
    if (!safeEqual(String(commit || ""), manifest.commit)) {
      throw new Error("knowledge_commit_not_current");
    }
    const body = this.git(
      ["archive", "--format=zip", manifest.commit],
      this.maxArchiveBytes + 1,
    );
    if (body.length > this.maxArchiveBytes) {
      throw new Error("knowledge_archive_too_large");
    }
    return {
      schema: "guanghu.hololake-knowledge-archive/v1",
      repository: this.repositoryId,
      commit: manifest.commit,
      sha256: crypto.createHash("sha256").update(body).digest("hex"),
      content_type: "application/zip",
      body,
    };
  }

  git(args, maxBuffer = 1024 * 1024) {
    return childProcess.execFileSync(
      "git",
      [`--git-dir=${this.repositoryPath}`, ...args],
      {
        encoding: "buffer",
        maxBuffer,
        stdio: ["ignore", "pipe", "pipe"],
      },
    );
  }
}

class HoloLakeAiGateway {
  constructor(options = {}) {
    this.fetchImpl = options.fetchImpl || fetch;
    this.maxMessages = Math.max(1, Number(options.maxMessages || 64));
    this.maxInputCharacters = Math.max(
      1,
      Number(options.maxInputCharacters || 120_000),
    );
    this.maxOutputTokens = Math.max(
      16,
      Number(options.maxOutputTokens || 16_384),
    );
    this.maxResponseBytes = Math.max(
      64 * 1024,
      Number(options.maxResponseBytes || 2 * 1024 * 1024),
    );
    this.providers = normalizeProviders(options.providers || {});
  }

  catalog() {
    return {
      schema: "guanghu.hololake-ai-catalog/v1",
      providers: Object.entries(this.providers).map(([id, provider]) => ({
        id,
        name: provider.name || id,
        models: provider.models,
      })),
    };
  }

  async execute(request = {}) {
    const providerId = String(request.provider || "");
    const provider = this.providers[providerId];
    if (!provider) throw new Error("ai_provider_not_registered");
    const model = String(request.model || "");
    if (!provider.models.includes(model)) {
      throw new Error("ai_model_not_registered");
    }
    const messages = validateMessages(
      request.messages,
      this.maxMessages,
      this.maxInputCharacters,
    );
    const maxTokens = Number(request.max_tokens || 1024);
    if (
      !Number.isInteger(maxTokens)
      || maxTokens < 1
      || maxTokens > this.maxOutputTokens
    ) {
      throw new Error("ai_output_limit_exceeded");
    }

    const response = await this.fetchImpl(`${provider.baseUrl}/chat/completions`, {
      method: "POST",
      headers: {
        authorization: `Bearer ${provider.apiKey}`,
        "content-type": "application/json",
      },
      body: JSON.stringify({
        model,
        messages,
        max_tokens: maxTokens,
        stream: false,
      }),
      signal: AbortSignal.timeout(provider.timeoutMs),
    });
    const text = await response.text();
    if (Buffer.byteLength(text) > this.maxResponseBytes) {
      throw new Error("ai_response_too_large");
    }
    if (!response.ok) {
      const error = new Error("ai_upstream_failed");
      error.status = response.status;
      throw error;
    }
    let parsed;
    try {
      parsed = JSON.parse(text);
    } catch {
      throw new Error("ai_upstream_invalid_json");
    }
    return {
      ok: true,
      response: parsed,
      receipt: {
        schema: "guanghu.hololake-ai-receipt/v1",
        state: "executed",
        provider: providerId,
        model,
        upstream_status: response.status,
        occurred_at: Date.now() / 1000,
      },
    };
  }
}

function loadAiProviders(file) {
  if (!file || !fs.existsSync(file)) return {};
  const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
  if (!parsed || parsed.schema !== "guanghu.hololake-ai-providers/v1") {
    throw new Error("ai_provider_registry_invalid");
  }
  return parsed.providers || {};
}

function normalizeProviders(providers) {
  const normalized = {};
  for (const [id, value] of Object.entries(providers)) {
    if (!/^[a-z0-9._-]{1,64}$/.test(id) || !value) continue;
    const baseUrl = String(value.baseUrl || value.base_url || "").replace(/\/$/, "");
    const apiKey = String(value.apiKey || value.api_key || "");
    const models = Array.isArray(value.models)
      ? value.models.map(String).filter(model => /^[A-Za-z0-9._:/-]{1,160}$/.test(model))
      : [];
    let parsed;
    try {
      parsed = new URL(baseUrl);
    } catch {
      continue;
    }
    if (parsed.protocol !== "https:" || !apiKey || models.length === 0) continue;
    normalized[id] = {
      baseUrl,
      apiKey,
      name: String(value.name || id).slice(0, 120),
      models,
      timeoutMs: Math.max(1_000, Number(value.timeoutMs || value.timeout_ms || 120_000)),
    };
  }
  return normalized;
}

function validateMessages(messages, maxMessages, maxInputCharacters) {
  if (!Array.isArray(messages) || messages.length < 1) {
    throw new Error("ai_messages_required");
  }
  if (messages.length > maxMessages) throw new Error("ai_input_too_large");
  let characters = 0;
  const normalized = messages.map(message => {
    if (
      !message
      || !["system", "user", "assistant"].includes(message.role)
      || typeof message.content !== "string"
    ) {
      throw new Error("ai_message_invalid");
    }
    characters += message.content.length;
    return { role: message.role, content: message.content };
  });
  if (characters > maxInputCharacters) throw new Error("ai_input_too_large");
  return normalized;
}

function safeEqual(left, right) {
  const a = Buffer.from(String(left));
  const b = Buffer.from(String(right));
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

module.exports = {
  HoloLakeAiGateway,
  HoloLakeKnowledgeProvider,
  loadAiProviders,
};
