import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
import type { InputJsonValue } from "@prisma/client/runtime/library";
import { hash } from "bcryptjs";
import { PrismaService } from "../prisma/prisma.service";
import { CryptoService } from "../crypto/crypto.service";

const BCRYPT_ROUNDS = 12;
const TEST_PROVIDER_TIMEOUT_MS = 8000;

const ONBOARDING_COMPLETED_KEY = "onboarding.completed";
const OIDC_ISSUER_URL_KEY = "oidc.issuerUrl";
const OIDC_CLIENT_ID_KEY = "oidc.clientId";
const OIDC_CLIENT_SECRET_KEY = "oidc.clientSecret";

interface ProviderModelInput {
  id: string;
  name?: string;
}

interface AddProviderInput {
  name: string;
  displayName: string;
  type: string;
  baseUrl?: string;
  apiKey?: string;
  models?: ProviderModelInput[];
}

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

  // Check if onboarding is completed
  async isCompleted(): Promise<boolean> {
    const completedFlag = await this.prisma.systemConfig.findUnique({
      where: { key: ONBOARDING_COMPLETED_KEY },
    });

    return completedFlag?.value === "true";
  }

  // Step 1: Create breakglass admin user
  async createBreakglassUser(
    username: string,
    password: string
  ): Promise<{ id: string; username: string }> {
    const breakglassCount = await this.prisma.breakglassUser.count();
    if (breakglassCount > 0) {
      throw new ConflictException("Breakglass user already exists");
    }

    const passwordHash = await hash(password, BCRYPT_ROUNDS);

    return this.prisma.breakglassUser.create({
      data: {
        username,
        passwordHash,
      },
      select: {
        id: true,
        username: true,
      },
    });
  }

  // Step 2: Configure OIDC provider (optional)
  async configureOidc(issuerUrl: string, clientId: string, clientSecret: string): Promise<void> {
    const encryptedSecret = this.crypto.encrypt(clientSecret);

    await Promise.all([
      this.upsertSystemConfig(OIDC_ISSUER_URL_KEY, issuerUrl, false),
      this.upsertSystemConfig(OIDC_CLIENT_ID_KEY, clientId, false),
      this.upsertSystemConfig(OIDC_CLIENT_SECRET_KEY, encryptedSecret, true),
    ]);
  }

  // Step 3: Add first LLM provider
  async addProvider(userId: string, data: AddProviderInput): Promise<{ id: string }> {
    const encryptedApiKey = data.apiKey ? this.crypto.encrypt(data.apiKey) : undefined;

    return this.prisma.llmProvider.create({
      data: {
        userId,
        name: data.name,
        displayName: data.displayName,
        type: data.type,
        baseUrl: data.baseUrl ?? null,
        apiKey: encryptedApiKey ?? null,
        models: (data.models ?? []) as unknown as InputJsonValue,
      },
      select: {
        id: true,
      },
    });
  }

  // Step 3b: Test LLM provider connection
  async testProvider(
    type: string,
    baseUrl?: string,
    apiKey?: string
  ): Promise<{ success: boolean; error?: string }> {
    const normalizedType = type.trim().toLowerCase();
    if (!normalizedType) {
      return { success: false, error: "Provider type is required" };
    }

    let probeUrl: string;
    try {
      probeUrl = this.buildProbeUrl(normalizedType, baseUrl);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      return { success: false, error: message };
    }

    const headers: Record<string, string> = {
      Accept: "application/json",
    };
    if (apiKey) {
      headers.Authorization = `Bearer ${apiKey}`;
    }

    try {
      const response = await fetch(probeUrl, {
        method: "GET",
        headers,
        signal: AbortSignal.timeout(TEST_PROVIDER_TIMEOUT_MS),
      });

      if (!response.ok) {
        return {
          success: false,
          error: `Provider returned ${String(response.status)} ${response.statusText}`.trim(),
        };
      }

      return { success: true };
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      return { success: false, error: message };
    }
  }

  // Step 4: Mark onboarding complete
  async complete(): Promise<void> {
    await this.upsertSystemConfig(ONBOARDING_COMPLETED_KEY, "true", false);
  }

  async getBreakglassUserId(): Promise<string> {
    const user = await this.prisma.breakglassUser.findFirst({
      where: { isActive: true },
      orderBy: { createdAt: "asc" },
      select: { id: true },
    });

    if (!user) {
      throw new BadRequestException("Create a breakglass user before adding a provider");
    }

    return user.id;
  }

  private async upsertSystemConfig(key: string, value: string, encrypted: boolean): Promise<void> {
    await this.prisma.systemConfig.upsert({
      where: { key },
      create: { key, value, encrypted },
      update: { value, encrypted },
    });
  }

  private buildProbeUrl(type: string, baseUrl?: string): string {
    const resolvedBaseUrl = baseUrl ?? this.getDefaultProviderBaseUrl(type);
    const normalizedBaseUrl = resolvedBaseUrl.endsWith("/")
      ? resolvedBaseUrl
      : `${resolvedBaseUrl}/`;
    const endpointPath = type === "ollama" ? "api/tags" : "models";

    return new URL(endpointPath, normalizedBaseUrl).toString();
  }

  private getDefaultProviderBaseUrl(type: string): string {
    if (type === "ollama") {
      return "http://localhost:11434";
    }

    return "https://api.openai.com/v1";
  }
}
