import { Injectable, NotFoundException } from "@nestjs/common";
import { hash } from "bcryptjs";
import type { Prisma } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import { CryptoService } from "../crypto/crypto.service";
import type {
  CreateProviderDto,
  ResetPasswordDto,
  UpdateAgentConfigDto,
  UpdateOidcDto,
  UpdateProviderDto,
} from "./fleet-settings.dto";

const BCRYPT_ROUNDS = 12;
const DEFAULT_PROVIDER_API_TYPE = "openai-completions";
const OIDC_ISSUER_KEY = "oidc.issuerUrl";
const OIDC_CLIENT_ID_KEY = "oidc.clientId";
const OIDC_CLIENT_SECRET_KEY = "oidc.clientSecret";
const OIDC_KEYS = [OIDC_ISSUER_KEY, OIDC_CLIENT_ID_KEY, OIDC_CLIENT_SECRET_KEY] as const;

export interface FleetProviderResponse {
  id: string;
  name: string;
  displayName: string;
  type: string;
  baseUrl: string | null;
  isActive: boolean;
  models: unknown;
}

export interface FleetAgentConfigResponse {
  primaryModel: string | null;
  fallbackModels: unknown[];
  personality: string | null;
}

export interface OidcConfigResponse {
  issuerUrl?: string;
  clientId?: string;
  configured: boolean;
}

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

  // --- LLM Provider CRUD (per-user scoped) ---

  async listProviders(userId: string): Promise<FleetProviderResponse[]> {
    return this.prisma.llmProvider.findMany({
      where: { userId },
      select: {
        id: true,
        name: true,
        displayName: true,
        type: true,
        baseUrl: true,
        isActive: true,
        models: true,
      },
      orderBy: { createdAt: "asc" },
    });
  }

  async getProvider(userId: string, providerId: string): Promise<FleetProviderResponse> {
    const provider = await this.prisma.llmProvider.findFirst({
      where: {
        id: providerId,
        userId,
      },
      select: {
        id: true,
        name: true,
        displayName: true,
        type: true,
        baseUrl: true,
        isActive: true,
        models: true,
      },
    });

    if (!provider) {
      throw new NotFoundException(`Provider ${providerId} not found`);
    }

    return provider;
  }

  async createProvider(userId: string, data: CreateProviderDto): Promise<{ id: string }> {
    const provider = await this.prisma.llmProvider.create({
      data: {
        userId,
        name: data.name,
        displayName: data.displayName,
        type: data.type,
        baseUrl: data.baseUrl ?? null,
        apiKey: data.apiKey ? this.crypto.encrypt(data.apiKey) : null,
        apiType: data.apiType ?? DEFAULT_PROVIDER_API_TYPE,
        models: (data.models ?? []) as Prisma.InputJsonValue,
      },
      select: {
        id: true,
      },
    });

    return provider;
  }

  async updateProvider(userId: string, providerId: string, data: UpdateProviderDto): Promise<void> {
    await this.assertProviderOwnership(userId, providerId);

    const updateData: Prisma.LlmProviderUpdateInput = {};
    if (data.displayName !== undefined) {
      updateData.displayName = data.displayName;
    }
    if (data.baseUrl !== undefined) {
      updateData.baseUrl = data.baseUrl;
    }
    if (data.isActive !== undefined) {
      updateData.isActive = data.isActive;
    }
    if (data.models !== undefined) {
      updateData.models = data.models as Prisma.InputJsonValue;
    }
    if (data.apiKey !== undefined) {
      updateData.apiKey = data.apiKey.length > 0 ? this.crypto.encrypt(data.apiKey) : null;
    }

    await this.prisma.llmProvider.update({
      where: { id: providerId },
      data: updateData,
    });
  }

  async deleteProvider(userId: string, providerId: string): Promise<void> {
    await this.assertProviderOwnership(userId, providerId);

    await this.prisma.llmProvider.delete({
      where: { id: providerId },
    });
  }

  // --- User Agent Config ---

  async getAgentConfig(userId: string): Promise<FleetAgentConfigResponse> {
    const config = await this.prisma.userAgentConfig.findUnique({
      where: { userId },
      select: {
        primaryModel: true,
        fallbackModels: true,
        personality: true,
      },
    });

    if (!config) {
      return {
        primaryModel: null,
        fallbackModels: [],
        personality: null,
      };
    }

    return {
      primaryModel: config.primaryModel,
      fallbackModels: this.normalizeJsonArray(config.fallbackModels),
      personality: config.personality,
    };
  }

  async updateAgentConfig(userId: string, data: UpdateAgentConfigDto): Promise<void> {
    const updateData: Prisma.UserAgentConfigUpdateInput = {};
    if (data.primaryModel !== undefined) {
      updateData.primaryModel = data.primaryModel;
    }
    if (data.personality !== undefined) {
      updateData.personality = data.personality;
    }
    if (data.fallbackModels !== undefined) {
      updateData.fallbackModels = data.fallbackModels as Prisma.InputJsonValue;
    }

    const createData: Prisma.UserAgentConfigCreateInput = {
      userId,
      fallbackModels: (data.fallbackModels ?? []) as Prisma.InputJsonValue,
      ...(data.primaryModel !== undefined ? { primaryModel: data.primaryModel } : {}),
      ...(data.personality !== undefined ? { personality: data.personality } : {}),
    };

    await this.prisma.userAgentConfig.upsert({
      where: { userId },
      create: createData,
      update: updateData,
    });
  }

  // --- OIDC Config (admin only) ---

  async getOidcConfig(): Promise<OidcConfigResponse> {
    const entries = await this.prisma.systemConfig.findMany({
      where: {
        key: {
          in: [...OIDC_KEYS],
        },
      },
      select: {
        key: true,
        value: true,
      },
    });

    const byKey = new Map(entries.map((entry) => [entry.key, entry.value]));
    const issuerUrl = byKey.get(OIDC_ISSUER_KEY);
    const clientId = byKey.get(OIDC_CLIENT_ID_KEY);
    const hasSecret = byKey.has(OIDC_CLIENT_SECRET_KEY);

    return {
      ...(issuerUrl ? { issuerUrl } : {}),
      ...(clientId ? { clientId } : {}),
      configured: Boolean(issuerUrl && clientId && hasSecret),
    };
  }

  async updateOidcConfig(data: UpdateOidcDto): Promise<void> {
    const encryptedSecret = this.crypto.encrypt(data.clientSecret);

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

  async deleteOidcConfig(): Promise<void> {
    await this.prisma.systemConfig.deleteMany({
      where: {
        key: {
          in: [...OIDC_KEYS],
        },
      },
    });
  }

  // --- Breakglass (admin only) ---

  async resetBreakglassPassword(
    username: ResetPasswordDto["username"],
    newPassword: ResetPasswordDto["newPassword"]
  ): Promise<void> {
    const user = await this.prisma.breakglassUser.findUnique({
      where: { username },
      select: { id: true },
    });

    if (!user) {
      throw new NotFoundException(`Breakglass user ${username} not found`);
    }

    const passwordHash = await hash(newPassword, BCRYPT_ROUNDS);

    await this.prisma.breakglassUser.update({
      where: { id: user.id },
      data: { passwordHash },
    });
  }

  private async assertProviderOwnership(userId: string, providerId: string): Promise<void> {
    const provider = await this.prisma.llmProvider.findFirst({
      where: {
        id: providerId,
        userId,
      },
      select: {
        id: true,
      },
    });

    if (!provider) {
      throw new NotFoundException(`Provider ${providerId} not found`);
    }
  }

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

  private normalizeJsonArray(value: unknown): unknown[] {
    return Array.isArray(value) ? value : [];
  }
}
