import type { PrismaClient, Llm, Prisma, LlmKind, LlmStatus } from '@prisma/client';

export interface CreateLlmInput {
  name: string;
  type: string;
  model: string;
  url?: string;
  tier?: string;
  description?: string;
  apiKeySecretId?: string | null;
  apiKeySecretKey?: string | null;
  extraConfig?: Record<string, unknown>;
  // v4: optional pool key. NULL = "pool of 1" (effective key falls back to `name`).
  poolName?: string | null;
  // v7: per-user RBAC scoping. ownerId is set by the service layer to
  // the authenticated caller's User.id; visibility defaults to 'public'
  // and gets flipped to 'private' for mcplocal-published virtuals.
  ownerId?: string | null;
  visibility?: 'public' | 'private';
  // Virtual-provider lifecycle (omit for kind=public).
  kind?: LlmKind;
  providerSessionId?: string | null;
  status?: LlmStatus;
  lastHeartbeatAt?: Date | null;
  inactiveSince?: Date | null;
}

export interface UpdateLlmInput {
  type?: string;
  model?: string;
  url?: string;
  tier?: string;
  description?: string;
  apiKeySecretId?: string | null;
  apiKeySecretKey?: string | null;
  extraConfig?: Record<string, unknown>;
  poolName?: string | null;
  // v7: ownerId immutable at update time (use a separate transfer flow if
  // ever needed). Visibility is mutable so an operator can flip a
  // virtual Llm to public for org-wide sharing without re-creating it.
  visibility?: 'public' | 'private';
  // Virtual-provider lifecycle. VirtualLlmService is the only writer for
  // these in v1; the public CRUD path leaves them undefined.
  kind?: LlmKind;
  providerSessionId?: string | null;
  status?: LlmStatus;
  lastHeartbeatAt?: Date | null;
  inactiveSince?: Date | null;
}

export interface ILlmRepository {
  findAll(): Promise<Llm[]>;
  findById(id: string): Promise<Llm | null>;
  findByName(name: string): Promise<Llm | null>;
  findByTier(tier: string): Promise<Llm[]>;
  /**
   * v4: members of an effective pool, ordered by name. The pool is
   * defined as: `WHERE poolName = $1 OR (poolName IS NULL AND name = $1)`.
   * For a non-pooled Llm this returns exactly the row whose own name is
   * the key. For a pool of N, returns all N members.
   */
  findByPoolName(poolName: string): Promise<Llm[]>;
  create(data: CreateLlmInput): Promise<Llm>;
  update(id: string, data: UpdateLlmInput): Promise<Llm>;
  delete(id: string): Promise<void>;
  // Virtual-provider lifecycle helpers (called by VirtualLlmService).
  findBySessionId(sessionId: string): Promise<Llm[]>;
  findStaleVirtuals(heartbeatCutoff: Date): Promise<Llm[]>;
  findExpiredInactives(deletionCutoff: Date): Promise<Llm[]>;
}

export class LlmRepository implements ILlmRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findAll(): Promise<Llm[]> {
    return this.prisma.llm.findMany({ orderBy: { name: 'asc' } });
  }

  async findById(id: string): Promise<Llm | null> {
    return this.prisma.llm.findUnique({ where: { id } });
  }

  async findByName(name: string): Promise<Llm | null> {
    return this.prisma.llm.findUnique({ where: { name } });
  }

  async findByTier(tier: string): Promise<Llm[]> {
    return this.prisma.llm.findMany({ where: { tier }, orderBy: { name: 'asc' } });
  }

  async findByPoolName(poolName: string): Promise<Llm[]> {
    return this.prisma.llm.findMany({
      where: {
        OR: [
          { poolName },
          // Solo rows fall back to their own `name` as the effective pool
          // key. Querying for "qwen-pool" must also pick up a solo row
          // whose name is "qwen-pool" (poolName=null) — otherwise existing
          // single-Llm targets would silently disappear from the pool view.
          { AND: [{ poolName: null }, { name: poolName }] },
        ],
      },
      orderBy: { name: 'asc' },
    });
  }

  async create(data: CreateLlmInput): Promise<Llm> {
    return this.prisma.llm.create({
      data: {
        name: data.name,
        type: data.type,
        model: data.model,
        url: data.url ?? '',
        tier: data.tier ?? 'fast',
        description: data.description ?? '',
        apiKeySecretId: data.apiKeySecretId ?? null,
        apiKeySecretKey: data.apiKeySecretKey ?? null,
        extraConfig: (data.extraConfig ?? {}) as Prisma.InputJsonValue,
        ...(data.poolName !== undefined ? { poolName: data.poolName } : {}),
        ...(data.ownerId !== undefined ? { ownerId: data.ownerId } : {}),
        ...(data.visibility !== undefined ? { visibility: data.visibility } : {}),
        ...(data.kind !== undefined ? { kind: data.kind } : {}),
        ...(data.providerSessionId !== undefined ? { providerSessionId: data.providerSessionId } : {}),
        ...(data.status !== undefined ? { status: data.status } : {}),
        ...(data.lastHeartbeatAt !== undefined ? { lastHeartbeatAt: data.lastHeartbeatAt } : {}),
        ...(data.inactiveSince !== undefined ? { inactiveSince: data.inactiveSince } : {}),
      },
    });
  }

  async update(id: string, data: UpdateLlmInput): Promise<Llm> {
    const updateData: Prisma.LlmUpdateInput = {};
    if (data.type !== undefined) updateData.type = data.type;
    if (data.model !== undefined) updateData.model = data.model;
    if (data.url !== undefined) updateData.url = data.url;
    if (data.tier !== undefined) updateData.tier = data.tier;
    if (data.description !== undefined) updateData.description = data.description;
    if (data.apiKeySecretId !== undefined) {
      updateData.apiKeySecret = data.apiKeySecretId === null
        ? { disconnect: true }
        : { connect: { id: data.apiKeySecretId } };
    }
    if (data.apiKeySecretKey !== undefined) updateData.apiKeySecretKey = data.apiKeySecretKey;
    if (data.extraConfig !== undefined) updateData.extraConfig = data.extraConfig as Prisma.InputJsonValue;
    if (data.poolName !== undefined) updateData.poolName = data.poolName;
    if (data.visibility !== undefined) updateData.visibility = data.visibility;
    if (data.kind !== undefined) updateData.kind = data.kind;
    if (data.providerSessionId !== undefined) updateData.providerSessionId = data.providerSessionId;
    if (data.status !== undefined) updateData.status = data.status;
    if (data.lastHeartbeatAt !== undefined) updateData.lastHeartbeatAt = data.lastHeartbeatAt;
    if (data.inactiveSince !== undefined) updateData.inactiveSince = data.inactiveSince;
    return this.prisma.llm.update({ where: { id }, data: updateData });
  }

  async delete(id: string): Promise<void> {
    await this.prisma.llm.delete({ where: { id } });
  }

  // ── Virtual-provider lifecycle queries ──

  async findBySessionId(sessionId: string): Promise<Llm[]> {
    return this.prisma.llm.findMany({
      where: { providerSessionId: sessionId },
      orderBy: { name: 'asc' },
    });
  }

  /**
   * Virtuals whose lastHeartbeatAt is older than the cutoff and are still
   * marked active. The GC sweep flips these to `inactive`. Public rows
   * never have lastHeartbeatAt set so they're naturally excluded by the
   * non-null compare on `lt`.
   */
  async findStaleVirtuals(heartbeatCutoff: Date): Promise<Llm[]> {
    return this.prisma.llm.findMany({
      where: {
        kind: 'virtual',
        status: 'active',
        lastHeartbeatAt: { lt: heartbeatCutoff },
      },
    });
  }

  /**
   * Virtuals that have been inactive longer than the deletion cutoff (4h
   * by default). The GC sweep removes these.
   */
  async findExpiredInactives(deletionCutoff: Date): Promise<Llm[]> {
    return this.prisma.llm.findMany({
      where: {
        kind: 'virtual',
        status: 'inactive',
        inactiveSince: { lt: deletionCutoff },
      },
    });
  }
}
