// src/web-cli.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved

import env from "./config/env.js";
const aiEnv: IAiEnvironment = {
  NODE_ENV: env.NODE_ENV || "develop",
  services: {
    google: {
      cse: {
        apiKey: env.google.cse.apiKey,
        engineId: env.google.cse.engineId,
      },
    },
  },
};
import { v4 as uuidv4 } from "uuid";

import "./lib/db.js";

/*
 * Models
 */

import ApiClient, { ApiClientStatus, IApiClient } from "./models/api-client.js";
import User from "./models/user.js";
import AiProvider from "./models/ai-provider.js";

/*
 * Services
 */

import {
  startServices,
  stopServices,
  ApiClientService,
  UserService,
  CryptoService,
} from "./services/index.js";

/*
 * App Logic
 */

import { createAiApi, IAiEnvironment, type IAiLogger } from "@gadget/ai";

import {
  IUser,
  type IAiModel,
  type IAiModelCapabilities,
  type IAiModelSettings,
} from "@gadget/api";

import { DtpProcess } from "./lib/process.js";

class DtpWebCli extends DtpProcess {
  get name(): string {
    return "DtpWebCli";
  }
  get slug(): string {
    return "dtp-web-cli";
  }

  constructor() {
    super();
  }

  async run(argv: string[]): Promise<void> {
    const cmd = argv.shift();
    if (!cmd) {
      throw new Error("must specify command");
    }
    switch (cmd) {
      case "admin":
        return this.onAdminCmd(argv);

      case "api-client":
        return this.onApiClientCmd(argv);

      case "user":
        return this.onUserCmd(argv);

      case "provider":
        return this.onProviderCmd(argv);

      default:
        break;
    }
    throw new Error(`unknown command: ${cmd}`);
  }

  async onAdminCmd(argv: string[]): Promise<void> {
    const action = argv.shift();
    if (!action) {
      throw new Error("must specify user command action");
    }
    switch (action) {
      case "grant":
        return this.onAdminGrant(argv);
      case "revoke":
        return this.onAdminRevoke(argv);
      default:
        break;
    }
    throw new Error(`unknown user command action: ${action}`);
  }

  async onAdminGrant(argv: string[]): Promise<void> {
    const email = argv.shift();
    if (!email) {
      throw new Error("must specify email for admin grant");
    }
    const user = await User.findOne({
      email_lc: email.trim().toLowerCase(),
    });
    if (!user) {
      throw new Error(`user ${email} not found`);
    }

    user.flags.isAdmin = true;
    await user.save();

    this.log.info(`admin rights granted to ${email}`);
  }

  async onAdminRevoke(argv: string[]): Promise<void> {
    const email = argv.shift();
    if (!email) {
      throw new Error("must specify email for admin revoke");
    }

    const user = await User.findOne({
      email_lc: email.trim().toLowerCase(),
    });
    if (!user) {
      throw new Error(`user ${email} not found`);
    }

    user.flags.isAdmin = false;
    await user.save();

    this.log.info(`admin rights revoked from ${email}`);
  }

  async onApiClientCmd(argv: string[]): Promise<void> {
    const action = argv.shift();
    if (!action) {
      throw new Error("must specify api client command action");
    }
    switch (action) {
      case "add":
        return this.onApiClientAdd(argv);

      case "ls":
        return this.onApiClientList(argv);

      case "status":
        return this.onApiClientSetStatus(argv);

      case "remove":
        return this.onApiClientRemove(argv);

      default:
        break;
    }
    throw new Error(`unknown api client command action: ${action}`);
  }

  async onApiClientAdd(argv: string[]): Promise<void> {
    const name = argv.shift();
    const description = argv.shift();

    const email = argv.shift();
    let user;
    if (email) {
      user = await UserService.getByEmail(email);
      if (!user) {
        throw new Error("user not found");
      }
    }

    const definition: Partial<IApiClient> = {
      name,
      description,
    };
    if (user) {
      definition.user = user._id;
    }

    const client = await ApiClientService.create(definition);
    this.printApiClientList([client]);
  }

  async onApiClientList(_argv: string[]): Promise<void> {
    const clients: IApiClient[] = await ApiClient.find({
      status: ApiClientStatus.Active,
    })
      .sort({ name: 1 })
      .populate([{ path: "user", select: "-passwordSalt -password" }])
      .lean();
    this.printApiClientList(clients);
  }

  printApiClientList(clients: IApiClient[]) {
    console.log(
      "Name".padEnd(20),
      "Gadget Key".padEnd(21),
      "Secret".padEnd(36),
      "User ID".padEnd(21),
      "Email",
    );
    console.log(
      "--------------------------------------------------------------------------------",
    );
    for (const client of clients) {
      let log = `${client.name.padEnd(20)} ${client._id.toString()} ${client.secret}`;
      if (client.user) {
        const user = client.user as IUser;
        log += ` ${user._id} ${user.email}`;
      }
      console.log(log);
    }
  }

  async onApiClientSetStatus(argv: string[]): Promise<void> {
    const clientId = argv.shift();
    if (!clientId) {
      throw new Error("client ID is required");
    }
    const client = await ApiClientService.getById(clientId);
    if (!client) {
      throw new Error("Client not found");
    }

    const status = argv.shift() as ApiClientStatus;
    if (!status) {
      throw new Error("New client status is required");
    }

    await ApiClientService.setStatus(client, status);
    this.log.info("API client status updated", { _id: clientId, status });
  }

  async onApiClientRemove(argv: string[]): Promise<void> {
    const clientId = argv.shift();
    if (!clientId) {
      throw new Error("client ID is required");
    }
    const client = await ApiClientService.getById(clientId);
    if (!client) {
      throw new Error("Client not found");
    }
    await ApiClientService.remove(client);
    this.log.info("API client removed", { _id: clientId });
  }

  async onUserCmd(argv: string[]): Promise<void> {
    const action = argv.shift();
    if (!action) {
      throw new Error("must specify user command action");
    }
    switch (action) {
      case "add":
        return this.onUserAdd(argv);
      case "view":
        return this.onUserView(argv);
      case "password":
        return this.onUserPassword(argv);
      case "remove":
        return this.onUserRemove(argv);
      case "ban":
        return this.onUserBan(argv);
      default:
        break;
    }
    throw new Error(`unknown user command action: ${action}`);
  }

  async onUserAdd(argv: string[]): Promise<void> {
    const email = argv.shift();
    if (!email) {
      throw new Error("must specify email address");
    }

    const password = argv.shift();
    if (!password) {
      throw new Error("must specify password");
    }

    const displayName: string | undefined = argv.shift();

    const user = await UserService.create(email, password, displayName);

    this.log.info(`user created: id:${user._id}, email:${user.email}`);
  }

  async onUserView(argv: string[]): Promise<void> {
    const email = argv.shift();
    if (!email) {
      throw new Error("must specify email address");
    }

    const user = await UserService.getByEmail(email);
    if (!user) {
      throw new Error("user not found");
    }

    this.log.info("user account", user);
  }

  async onUserRemove(argv: string[]): Promise<void> {
    let email = argv.shift();
    if (!email) {
      throw new Error("must specify email address");
    }

    email = email.trim().toLowerCase();
    const user = await User.findOne({ email }).lean();
    if (!user) {
      throw new Error(`user not found: ${email}`);
    }

    await User.deleteOne({ _id: user._id });
    this.log.info(`user ${email} removed`);
  }

  async onUserBan(argv: string[]): Promise<void> {
    const email = argv.shift();
    if (!email) {
      throw new Error("must specify email address");
    }

    const email_lc = email.trim().toLowerCase();
    const user = await User.findOne({ email_lc }).lean();
    if (!user) {
      throw new Error(`user not found: ${email}`);
    }

    await User.updateOne(
      { _id: user._id },
      {
        $set: {
          "flags.isBanned": true,
        },
      },
    );
    this.log.info(`user ${email} banned`);
  }

  async onUserUnban(argv: string[]): Promise<void> {
    const email = argv.shift();
    if (!email) {
      throw new Error("must specify email address");
    }

    const email_lc = email.trim().toLowerCase();
    const user = await User.findOne({ email_lc }).lean();
    if (!user) {
      throw new Error(`user not found: ${email}`);
    }

    await User.updateOne(
      { _id: user._id },
      {
        $set: {
          "flags.isBanned": false,
        },
      },
    );
    this.log.info(`user ${email} unbanned`);
  }

  async onUserPassword(argv: string[]): Promise<void> {
    let email = argv.shift();
    if (!email) {
      throw new Error("must specify email address");
    }

    const password = argv.shift();
    if (!password) {
      throw new Error("must specify password");
    }

    const email_lc = email.trim().toLowerCase();
    const user = await User.findOne({ email_lc });
    if (!user) {
      throw new Error(`user not found: ${email}`);
    }

    user.passwordSalt = uuidv4();
    user.password = await CryptoService.maskPassword(
      user.passwordSalt,
      password,
    );
    await user.save();

    this.log.info(`user ${email} password changed`);
  }

  async onProviderCmd(argv: string[]): Promise<void> {
    const action = argv.shift();
    if (!action) {
      throw new Error("must specify provider command action");
    }
    switch (action) {
      case "add":
        return this.onProviderAdd(argv);
      case "ls":
        return this.onProviderList(argv);
      case "status":
        return this.onProviderSetStatus(argv);
      case "remove":
        return this.onProviderRemove(argv);
      case "probe":
        return this.onProviderProbe(argv);
      default:
        break;
    }
    throw new Error(`unknown provider command action: ${action}`);
  }

  async onProviderAdd(argv: string[]): Promise<void> {
    const name = argv.shift();
    if (!name) {
      throw new Error("must specify provider name");
    }

    const sdkType = argv.shift();
    if (!sdkType) {
      throw new Error("must specify SDK type (ollama or openai)");
    }
    if (sdkType !== "ollama" && sdkType !== "openai") {
      throw new Error("SDK type must be 'ollama' or 'openai'");
    }

    const baseUrl = argv.shift();
    if (!baseUrl) {
      throw new Error("must specify base URL");
    }

    const apiKey = argv.shift();
    if (sdkType === "openai" && !apiKey) {
      throw new Error("API key required for OpenAI providers");
    }

    // Check if provider with this name already exists
    const existing = await AiProvider.findOne({ baseUrl });
    if (existing) {
      throw new Error(`provider with name '${baseUrl}' already exists`);
    }

    const provider = new AiProvider({
      name,
      apiType: sdkType,
      baseUrl,
      apiKey: apiKey || "",
      enabled: true,
      models: [],
      lastModelRefresh: new Date(),
    });
    await provider.save();

    this.log.info("provider added", {
      _id: provider._id,
      name: provider.name,
      apiType: provider.apiType,
      baseUrl: provider.baseUrl,
    });

    // Auto-probe for models
    this.log.info("probing provider for models...");
    await this.onProviderProbe([provider._id]);
  }

  async onProviderList(_argv: string[]): Promise<void> {
    const providers = await AiProvider.find({}).sort({ name: 1 }).lean();

    console.log(
      "Name".padEnd(20),
      "ID".padEnd(24),
      "Type".padEnd(8),
      "URL".padEnd(30),
      "Models",
      "Enabled",
    );
    console.log(
      "------------------------------------------------------------------------------------------------------------",
    );
    for (const provider of providers) {
      console.log(
        provider.name.padEnd(20),
        provider._id.toString().padEnd(24),
        provider.apiType.padEnd(8),
        provider.baseUrl.padEnd(30),
        String(provider.models.length).padEnd(6),
        provider.enabled ? "Yes" : "No",
      );
    }
  }

  async onProviderSetStatus(argv: string[]): Promise<void> {
    const providerId = argv.shift();
    if (!providerId) {
      throw new Error("provider ID is required");
    }
    const provider = await AiProvider.findById(providerId);
    if (!provider) {
      throw new Error("Provider not found");
    }

    const status = argv.shift();
    if (!status) {
      throw new Error("New provider status is required (active or inactive)");
    }
    if (status !== "active" && status !== "inactive") {
      throw new Error("Status must be 'active' or 'inactive'");
    }

    provider.enabled = status === "active";
    await provider.save();

    this.log.info("Provider status updated", {
      _id: providerId,
      enabled: provider.enabled,
    });
  }

  async onProviderRemove(argv: string[]): Promise<void> {
    const providerId = argv.shift();
    if (!providerId) {
      throw new Error("provider ID is required");
    }
    const provider = await AiProvider.findById(providerId).select("+apiKey");
    if (!provider) {
      throw new Error("Provider not found");
    }
    this.log.info("Provider removed", {
      _id: providerId,
      name: provider.name,
    });
  }

  async onProviderProbe(argv: string[]): Promise<void> {
    const providerId = argv.shift();
    if (!providerId) {
      throw new Error("provider ID is required");
    }
    const provider = await AiProvider.findById(providerId).select("+apiKey");
    if (!provider) {
      throw new Error("Provider not found");
    }

    this.log.info("probing provider for models", {
      name: provider.name,
      apiType: provider.apiType,
    });

    const cliLogger: IAiLogger = {
      debug: async (msg, meta) => this.log.debug(msg, meta),
      info: async (msg, meta) => this.log.info(msg, meta),
      warn: async (msg, meta) => this.log.warn(msg, meta),
      error: async (msg, meta) => this.log.error(msg, meta),
    };

    const api = createAiApi(
      aiEnv,
      {
        _id: provider._id,
        name: provider.name,
        sdk: provider.apiType,
        baseUrl: provider.baseUrl,
        apiKey: provider.apiKey,
      },
      cliLogger,
    );

    this.log.info("fetching model list from provider...");
    const listResult = await api.listModels();
    this.log.info(`found ${listResult.models.length} models`, {
      count: listResult.models.length,
    });

    const models: IAiModel[] = [];
    for (const modelInfo of listResult.models) {
      this.log.info(`probing model: ${modelInfo.id}`);
      try {
        const probeResult = await api.probeModel(modelInfo.id);

        // Compute provider-specific inference settings
        const settings: IAiModelSettings = {
          ...(probeResult.settings as IAiModelSettings | undefined),
        };

        if (provider.apiType === 'ollama') {
          // Ollama: always override numPredict to -1 (unlimited) for agentic workflows
          // The model must generate until its natural stop token or context limit
          settings.numPredict = -1;
          // numCtx is already populated by probeResult.settings from extractSettings()
        } else if (provider.apiType === 'openai') {
          // OpenAI-compatible: set maxCompletionTokens to model's context window
          // This prevents compatible providers (Gab AI, etc.) from imposing low defaults
          settings.maxCompletionTokens = modelInfo.contextWindow || 16384;
        }

        const model: IAiModel = {
          id: modelInfo.id,
          name: modelInfo.name,
          parameterCount: modelInfo.parameterCount,
          parameterLabel: modelInfo.parameterLabel,
          contextWindow: modelInfo.contextWindow,
          capabilities: probeResult.capabilities as IAiModelCapabilities,
          settings,
        };

        models.push(model);
        this.log.info(`model probed successfully`, {
          model: model.id,
          capabilities: model.capabilities,
        });
      } catch (error) {
        this.log.error(`failed to probe model ${modelInfo.id}`, {
          error: (error as Error).message,
        });
      }
    }

    provider.models = models;
    provider.lastModelRefresh = new Date();
    await provider.save();

    this.log.info(`model discovery complete`, {
      totalModels: models.length,
      models: models.map((m) => ({
        id: m.id,
        name: m.name,
        capabilities: m.capabilities,
      })),
    });
  }
}

(async () => {
  try {
    await startServices();

    const cli = new DtpWebCli();
    await cli.run(process.argv.slice(2));

    await stopServices();

    process.exit(0);
  } catch (error) {
    console.error((error as Error).message);
    process.exit(-1);
  }
})();
