import { Injectable, NotFoundException } from "@nestjs/common";
import type { LlmProvider } from "@prisma/client";
import { createHash, timingSafeEqual } from "node:crypto";
import { PrismaService } from "../prisma/prisma.service";
import { CryptoService } from "../crypto/crypto.service";

const DEFAULT_GATEWAY_PORT = 18789;
const DEFAULT_PRIMARY_MODEL = "openai/gpt-4o-mini";

type ContainerType = "user" | "system";

export interface ContainerTokenValidation {
  type: ContainerType;
  id: string;
}

type OpenClawModelMap = Record<string, Record<string, never>>;

interface OpenClawProviderConfig {
  apiKey?: string;
  baseUrl?: string;
  models: OpenClawModelMap;
}

interface OpenClawConfig {
  gateway: {
    mode: "local";
    port: number;
    bind: "lan";
    auth: { mode: "token" };
    http: {
      endpoints: {
        chatCompletions: { enabled: true };
      };
    };
  };
  agents: {
    defaults: {
      model: {
        primary: string;
      };
    };
  };
  models: {
    providers: Record<string, OpenClawProviderConfig>;
  };
}

@Injectable()
export class AgentConfigService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly crypto: CryptoService
  ) {}

  // Generate complete openclaw.json for a user container
  async generateUserConfig(userId: string): Promise<OpenClawConfig> {
    const [userAgentConfig, providers, userContainer] = await Promise.all([
      this.prisma.userAgentConfig.findUnique({
        where: { userId },
      }),
      this.prisma.llmProvider.findMany({
        where: {
          userId,
          isActive: true,
        },
        orderBy: {
          createdAt: "asc",
        },
      }),
      this.prisma.userContainer.findUnique({
        where: { userId },
      }),
    ]);

    if (!userContainer) {
      throw new NotFoundException(`User container not found for user ${userId}`);
    }

    const primaryModel =
      userAgentConfig?.primaryModel ??
      this.resolvePrimaryModelFromProviders(providers) ??
      DEFAULT_PRIMARY_MODEL;

    return this.buildOpenClawConfig(primaryModel, userContainer.gatewayPort, providers);
  }

  // Generate config for a system container
  async generateSystemConfig(containerId: string): Promise<OpenClawConfig> {
    const systemContainer = await this.prisma.systemContainer.findUnique({
      where: { id: containerId },
    });

    if (!systemContainer) {
      throw new NotFoundException(`System container ${containerId} not found`);
    }

    return this.buildOpenClawConfig(
      systemContainer.primaryModel || DEFAULT_PRIMARY_MODEL,
      systemContainer.gatewayPort,
      []
    );
  }

  async generateConfigForContainer(
    type: ContainerType,
    containerId: string
  ): Promise<OpenClawConfig> {
    if (type === "system") {
      return this.generateSystemConfig(containerId);
    }

    const userContainer = await this.prisma.userContainer.findUnique({
      where: { id: containerId },
      select: { userId: true },
    });

    if (!userContainer) {
      throw new NotFoundException(`User container ${containerId} not found`);
    }

    return this.generateUserConfig(userContainer.userId);
  }

  // Validate a container's bearer token
  async validateContainerToken(token: string): Promise<ContainerTokenValidation | null> {
    if (!token) {
      return null;
    }

    const [userContainers, systemContainers] = await Promise.all([
      this.prisma.userContainer.findMany({
        select: {
          id: true,
          gatewayToken: true,
        },
      }),
      this.prisma.systemContainer.findMany({
        select: {
          id: true,
          gatewayToken: true,
        },
      }),
    ]);

    let match: ContainerTokenValidation | null = null;

    for (const container of userContainers) {
      const storedToken = this.decryptContainerToken(container.gatewayToken);
      if (!match && storedToken && this.tokensEqual(storedToken, token)) {
        match = { type: "user", id: container.id };
      }
    }

    for (const container of systemContainers) {
      const storedToken = this.decryptContainerToken(container.gatewayToken);
      if (!match && storedToken && this.tokensEqual(storedToken, token)) {
        match = { type: "system", id: container.id };
      }
    }

    return match;
  }

  private buildOpenClawConfig(
    primaryModel: string,
    gatewayPort: number | null,
    providers: LlmProvider[]
  ): OpenClawConfig {
    return {
      gateway: {
        mode: "local",
        port: gatewayPort ?? DEFAULT_GATEWAY_PORT,
        bind: "lan",
        auth: { mode: "token" },
        http: {
          endpoints: {
            chatCompletions: { enabled: true },
          },
        },
      },
      agents: {
        defaults: {
          model: {
            primary: primaryModel,
          },
        },
      },
      models: {
        providers: this.buildProviderConfig(providers),
      },
    };
  }

  private buildProviderConfig(providers: LlmProvider[]): Record<string, OpenClawProviderConfig> {
    const providerConfig: Record<string, OpenClawProviderConfig> = {};

    for (const provider of providers) {
      const config: OpenClawProviderConfig = {
        models: this.extractModels(provider.models),
      };

      const apiKey = this.decryptIfNeeded(provider.apiKey);
      if (apiKey) {
        config.apiKey = apiKey;
      }

      if (provider.baseUrl) {
        config.baseUrl = provider.baseUrl;
      }

      providerConfig[provider.name] = config;
    }

    return providerConfig;
  }

  private extractModels(models: unknown): OpenClawModelMap {
    const modelMap: OpenClawModelMap = {};

    if (!Array.isArray(models)) {
      return modelMap;
    }

    for (const modelEntry of models) {
      if (typeof modelEntry === "string") {
        modelMap[modelEntry] = {};
        continue;
      }

      if (this.hasModelId(modelEntry)) {
        modelMap[modelEntry.id] = {};
      }
    }

    return modelMap;
  }

  private resolvePrimaryModelFromProviders(providers: LlmProvider[]): string | null {
    for (const provider of providers) {
      const modelIds = Object.keys(this.extractModels(provider.models));
      const firstModelId = modelIds[0];

      if (firstModelId) {
        return `${provider.name}/${firstModelId}`;
      }
    }

    return null;
  }

  private decryptIfNeeded(value: string | null | undefined): string | undefined {
    if (!value) {
      return undefined;
    }

    if (this.crypto.isEncrypted(value)) {
      return this.crypto.decrypt(value);
    }

    return value;
  }

  private decryptContainerToken(value: string): string | null {
    try {
      return this.decryptIfNeeded(value) ?? null;
    } catch {
      return null;
    }
  }

  private tokensEqual(left: string, right: string): boolean {
    const leftDigest = createHash("sha256").update(left, "utf8").digest();
    const rightDigest = createHash("sha256").update(right, "utf8").digest();
    return timingSafeEqual(leftDigest, rightDigest);
  }

  private hasModelId(modelEntry: unknown): modelEntry is { id: string } {
    if (typeof modelEntry !== "object" || modelEntry === null || !("id" in modelEntry)) {
      return false;
    }

    return typeof (modelEntry as { id?: unknown }).id === "string";
  }
}
