/// <reference lib="deno.ns" />

import { join } from "jsr:@std/path";
import type {
  ComposeContext,
  ComposeObject,
  ServiceDefinition,
} from "../routines/composeTypes";
import {
  addBackendDependency,
  BACKEND_MODEL_KEYS,
  type DetectedBackend,
  ensureV1Suffix,
  getAllActiveBackends,
  setEnvVars,
} from "../routines/backendIntegration";

const GENERATED_CONFIG =
  "./services/surfsense/configs/global_llm_config.generated.yaml";
const DUMMY_API_KEY = "sk-harbor";

function quote(value: string): string {
  return JSON.stringify(value || "");
}

function providerFor(backend: DetectedBackend): string {
  return backend.service === "ollama" ? "OLLAMA" : "OPENAI";
}

function apiBaseFor(backend: DetectedBackend): string {
  return backend.service === "ollama"
    ? backend.info.url
    : ensureV1Suffix(backend.info.url);
}

function isLikelyEmbeddingModel(model: string): boolean {
  const normalized = model.toLowerCase();
  return [
    "embed",
    "embedding",
    "bge",
    "e5",
    "minilm",
    "mxbai",
    "nomic",
    "rerank",
  ]
    .some((part) => normalized.includes(part));
}

function firstOllamaChatModel(defaultModels: string): string | undefined {
  return defaultModels
    .split(",")
    .map((model) => model.trim())
    .find((model) => model && !isLikelyEmbeddingModel(model));
}

async function modelFor(
  ctx: ComposeContext,
  backend: DetectedBackend,
): Promise<string | undefined> {
  const explicitModel = await ctx.env.getValue("surfsense.model");
  if (explicitModel) return explicitModel;

  if (backend.service === "ollama") {
    const defaultModels = await ctx.env.getValue("ollama.default.models");
    return firstOllamaChatModel(defaultModels) || "qwen3.5:4b";
  }

  if (backend.service === "llamacpp") {
    return undefined;
  }

  const modelKey = BACKEND_MODEL_KEYS[backend.service];
  if (modelKey) {
    const backendModel = await ctx.env.getValue(modelKey);
    if (backendModel) return backendModel;
  }

  return "local-model";
}

type LLMGenerationSettings = {
  temperature: number;
  maxTokens: number;
};

function configEntry(
  backend: DetectedBackend,
  model: string,
  index: number,
  settings: LLMGenerationSettings,
): string {
  const name = `Harbor ${backend.info.name}`;
  const slug = `harbor-${backend.service}`.replace(/[^a-z0-9-]/g, "-");

  return `  - id: ${-101 - index}
    name: ${quote(name)}
    description: ${quote(`${backend.info.name} provided by Harbor`)}
    billing_tier: "free"
    anonymous_enabled: true
    seo_enabled: false
    seo_slug: ${quote(slug)}
    quota_reserve_tokens: 4000
    provider: ${quote(providerFor(backend))}
    model_name: ${quote(model)}
    api_key: ${quote(DUMMY_API_KEY)}
    api_base: ${quote(apiBaseFor(backend))}
    rpm: 120
    tpm: 100000
    litellm_params:
      temperature: ${settings.temperature}
      max_tokens: ${settings.maxTokens}
    system_instructions: ""
    use_default_system_instructions: true
    citations_enabled: true`;
}

function renderConfig(entries: string[]): string {
  return `# Generated by Harbor. Do not edit directly.
# Configure via harbor config set surfsense.model ... and surfsense.llm.* settings.

router_settings:
  routing_strategy: "usage-based-routing"
  num_retries: 3
  allowed_fails: 3
  cooldown_time: 60

global_llm_configs:
${entries.join("\n")}
`;
}

function addVolume(
  service: ServiceDefinition | undefined,
  volume: string,
): void {
  if (!service) return;
  if (!Array.isArray(service.volumes)) service.volumes = [];
  if (!service.volumes.includes(volume)) service.volumes.push(volume);
}

function addDependency(
  service: ServiceDefinition | undefined,
  name: string,
  condition = "service_started",
): void {
  if (!service) return;

  if (service.depends_on && !Array.isArray(service.depends_on)) {
    service.depends_on[name] = { condition };
    return;
  }

  const existing = Array.isArray(service.depends_on) ? service.depends_on : [];
  const next: Record<string, { condition: string }> = {};
  for (const dep of existing) next[dep] = { condition: "service_started" };
  next[name] = { condition };
  service.depends_on = next;
}

function appendCsv(value: string, item: string): string {
  const items = value.split(",").map((part) => part.trim()).filter(Boolean);
  if (!items.includes(item)) items.push(item);
  return items.join(",");
}

function isDefaultLocalUrl(value: string): boolean {
  return !value || value.includes("localhost") || value.includes("127.0.0.1");
}

function parseNumber(value: string | undefined, fallback: number): number {
  if (!value) return fallback;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallback;
}

async function getLLMGenerationSettings(
  ctx: ComposeContext,
): Promise<LLMGenerationSettings> {
  const [temperature, maxTokens] = await Promise.all([
    ctx.env.getValue("surfsense.llm.temperature"),
    ctx.env.getValue("surfsense.llm.max.tokens"),
  ]);

  return {
    temperature: parseNumber(temperature, 0.2),
    maxTokens: Math.max(1, Math.trunc(parseNumber(maxTokens, 4000))),
  };
}

function isPrivateIPv4(ip: string): boolean {
  return ip.startsWith("10.") || ip.startsWith("192.168.") ||
    /^172\.(1[6-9]|2\d|3[0-1])\./.test(ip);
}

function getAddressableHost(): string {
  const candidates = Deno.networkInterfaces()
    .filter((iface) =>
      iface.family === "IPv4" && !iface.address.startsWith("127.")
    )
    .map((iface) => iface.address);

  return candidates.find(isPrivateIPv4) || candidates[0] || "localhost";
}

async function configurePublicUrls(ctx: ComposeContext): Promise<void> {
  const { compose, env } = ctx;
  const web = compose.services["surfsense-web"];
  const backend = compose.services["surfsense-backend"];
  if (!web || !backend) return;

  const [hostPort, frontendUrl, backendUrl, zeroUrl] = await Promise.all([
    env.getValue("surfsense.host.port"),
    env.getValue("surfsense.public.frontend.url"),
    env.getValue("surfsense.public.backend.url"),
    env.getValue("surfsense.public.zero.url"),
  ]);

  const origin = `http://${getAddressableHost()}:${hostPort || "34851"}`;
  const publicFrontendUrl = isDefaultLocalUrl(frontendUrl)
    ? origin
    : frontendUrl;
  const publicBackendUrl = isDefaultLocalUrl(backendUrl) ? origin : backendUrl;
  const publicZeroUrl = isDefaultLocalUrl(zeroUrl) ? `${origin}/zero` : zeroUrl;

  setEnvVars(web, {
    NEXT_PUBLIC_FASTAPI_BACKEND_URL: publicBackendUrl,
    NEXT_PUBLIC_ZERO_CACHE_URL: publicZeroUrl,
  });

  setEnvVars(backend, {
    NEXT_FRONTEND_URL: publicFrontendUrl,
  });
}

export default async function apply(
  ctx: ComposeContext,
): Promise<ComposeObject> {
  const { compose, services, dir } = ctx;

  if (
    !compose.services?.surfsense || !compose.services?.["surfsense-backend"]
  ) {
    return compose;
  }

  await configurePublicUrls(ctx);

  const backends = getAllActiveBackends(services)
    .filter((backend) =>
      backend.service === "ollama" || backend.service === "llamacpp"
    );

  if (backends.length === 0) {
    return compose;
  }

  const generationSettings = await getLLMGenerationSettings(ctx);
  const entries: string[] = [];
  for (const [index, backend] of backends.entries()) {
    const model = await modelFor(ctx, backend);
    if (model) {
      entries.push(configEntry(backend, model, index, generationSettings));
    }

    addBackendDependency(
      compose.services["surfsense-backend"],
      backend.service,
    );
    addBackendDependency(compose.services["surfsense-worker"], backend.service);

    if (backend.service === "ollama" && compose.services["ollama-init"]) {
      const defaults = await ctx.env.getValue("ollama.default.models");
      setEnvVars(compose.services["ollama-init"], {
        HARBOR_OLLAMA_DEFAULT_MODELS: appendCsv(defaults, model),
      });
      addDependency(
        compose.services["surfsense-backend"],
        "ollama-init",
        "service_healthy",
      );
      addDependency(
        compose.services["surfsense-worker"],
        "ollama-init",
        "service_healthy",
      );
    }
  }

  if (entries.length === 0) {
    return compose;
  }

  const configPath = join(dir, GENERATED_CONFIG);
  await Deno.mkdir(join(dir, "./services/surfsense/configs"), {
    recursive: true,
  });
  await Deno.writeTextFile(configPath, renderConfig(entries));

  const mount = `${GENERATED_CONFIG}:/app/app/config/global_llm_config.yaml:ro`;
  addVolume(compose.services["surfsense-backend"], mount);
  addVolume(compose.services["surfsense-worker"], mount);
  addVolume(compose.services["surfsense-beat"], mount);

  return compose;
}
