import type { ClickHouse } from "@internal/clickhouse";
import type { PrismaClientOrTransaction } from "~/db.server";
import { BasePresenter } from "./basePresenter.server";
import { z } from "zod";

const GenerationRowSchema = z.object({
  run_id: z.string(),
  span_id: z.string(),
  operation_id: z.string(),
  task_identifier: z.string(),
  response_model: z.string(),
  prompt_version: z.coerce.number(),
  input_tokens: z.coerce.number(),
  output_tokens: z.coerce.number(),
  total_cost: z.coerce.number(),
  duration_ms: z.coerce.number(),
  started_at: z.string(),
});

export type GenerationRow = {
  run_id: string;
  span_id: string;
  operation_id: string;
  task_identifier: string;
  response_model: string;
  prompt_version: number;
  input_tokens: number;
  output_tokens: number;
  total_cost: number;
  duration_ms: number;
  start_time: string;
};

export type GenerationsPagination = {
  next?: string;
};

export class PromptPresenter extends BasePresenter {
  private readonly clickhouse: ClickHouse;

  constructor(clickhouse: ClickHouse, replica?: PrismaClientOrTransaction) {
    super(undefined, replica);
    this.clickhouse = clickhouse;
  }

  async listPrompts(projectId: string, environmentId: string) {
    const prompts = await this._replica.prompt.findMany({
      where: {
        projectId,
        runtimeEnvironmentId: environmentId,
        archivedAt: null,
      },
      include: {
        versions: {
          where: {
            labels: { hasSome: ["current", "override"] },
          },
          select: {
            version: true,
            labels: true,
            model: true,
          },
        },
      },
      orderBy: { updatedAt: "desc" },
    });

    return prompts.map((p) => {
      const currentVersion = p.versions.find((v) => v.labels.includes("current"));
      const overrideVersion = p.versions.find((v) => v.labels.includes("override"));
      const hasOverride = !!overrideVersion;

      // Effective model: override > current version > prompt default
      const effectiveModel = overrideVersion?.model ?? currentVersion?.model ?? p.defaultModel;

      return {
        id: p.id,
        friendlyId: p.friendlyId,
        slug: p.slug,
        description: p.description,
        tags: p.tags,
        defaultModel: effectiveModel,
        currentVersion: currentVersion ? { version: currentVersion.version } : null,
        overrideVersion: overrideVersion ? { version: overrideVersion.version } : null,
        hasOverride,
        updatedAt: p.updatedAt,
      };
    });
  }

  async getUsageSparklines(
    environmentId: string,
    promptSlugs: string[]
  ): Promise<Record<string, number[]>> {
    if (promptSlugs.length === 0) return {};

    const queryFn = this.clickhouse.reader.query({
      name: "promptUsageSparklines",
      query: `SELECT
          prompt_slug,
          toStartOfHour(start_time) AS bucket,
          count() AS cnt
        FROM trigger_dev.llm_metrics_v1
        WHERE environment_id = {environmentId: String}
          AND prompt_slug IN {promptSlugs: Array(String)}
          AND start_time >= now() - INTERVAL 24 HOUR
        GROUP BY prompt_slug, bucket
        ORDER BY prompt_slug, bucket`,
      params: z.object({
        environmentId: z.string(),
        promptSlugs: z.array(z.string()),
      }),
      schema: z.object({
        prompt_slug: z.string(),
        bucket: z.string(),
        cnt: z.coerce.number(),
      }),
    });

    const [error, rows] = await queryFn({ environmentId, promptSlugs });
    if (error) {
      console.error("Prompt usage sparkline query failed:", error);
      return {};
    }

    // Build a map of slug -> 24 hourly buckets (use UTC to match ClickHouse)
    const now = new Date();
    const startHour = new Date(
      Date.UTC(
        now.getUTCFullYear(),
        now.getUTCMonth(),
        now.getUTCDate(),
        now.getUTCHours() - 23,
        0,
        0,
        0
      )
    );

    const bucketKeys: string[] = [];
    for (let i = 0; i < 24; i++) {
      const h = new Date(startHour.getTime() + i * 3600_000);
      // Format to match ClickHouse's toStartOfHour output: "YYYY-MM-DD HH:MM:SS"
      bucketKeys.push(h.toISOString().slice(0, 13).replace("T", " ") + ":00:00");
    }

    // Index rows by slug+bucket for fast lookup
    const rowMap = new Map<string, number>();
    for (const row of rows) {
      rowMap.set(`${row.prompt_slug}|${row.bucket}`, row.cnt);
    }

    const result: Record<string, number[]> = {};
    for (const slug of promptSlugs) {
      result[slug] = bucketKeys.map((key) => rowMap.get(`${slug}|${key}`) ?? 0);
    }

    return result;
  }

  async resolveVersion(promptId: string, options?: { version?: number; label?: string }) {
    if (options?.version != null) {
      return this._replica.promptVersion.findUnique({
        where: {
          promptId_version: {
            promptId,
            version: options.version,
          },
        },
      });
    }

    // Check for override first — dashboard edits take precedence
    const override = await this._replica.promptVersion.findFirst({
      where: {
        promptId,
        labels: { has: "override" },
      },
      orderBy: { version: "desc" },
    });

    if (override) {
      return override;
    }

    const label = options?.label ?? "current";
    return this._replica.promptVersion.findFirst({
      where: {
        promptId,
        labels: { has: label },
      },
      orderBy: { version: "desc" },
    });
  }

  async listVersions(promptId: string, limit: number = 50) {
    return this._replica.promptVersion.findMany({
      where: { promptId },
      orderBy: { version: "desc" },
      take: limit,
      select: {
        id: true,
        version: true,
        labels: true,
        source: true,
        model: true,
        textContent: true,
        commitMessage: true,
        contentHash: true,
        createdAt: true,
      },
    });
  }

  async listGenerations(options: {
    environmentId: string;
    promptSlug: string;
    promptVersions?: number[];
    startTime: Date;
    endTime: Date;
    cursor?: string;
    pageSize?: number;
    responseModels?: string[];
    operations?: string[];
    providers?: string[];
  }): Promise<{ generations: GenerationRow[]; pagination: GenerationsPagination }> {
    const pageSize = options.pageSize ?? 25;
    const decodedCursor = options.cursor ? decodeCursor(options.cursor) : null;

    const cursorClause = decodedCursor
      ? `AND (start_time < parseDateTimeBestEffort({cursorStartTime: String})
             OR (start_time = parseDateTimeBestEffort({cursorStartTime: String})
                 AND span_id < {cursorSpanId: String}))`
      : "";

    const versionClause = options.promptVersions?.length
      ? `AND prompt_version IN {promptVersions: Array(UInt32)}`
      : "";
    const modelClause = options.responseModels?.length
      ? `AND response_model IN {responseModels: Array(String)}`
      : "";
    const operationClause = options.operations?.length
      ? `AND operation_id IN {operations: Array(String)}`
      : "";
    const providerClause = options.providers?.length
      ? `AND gen_ai_system IN {providers: Array(String)}`
      : "";

    // Build a unique query name based on which optional filters are active
    const filterKey = [
      decodedCursor ? "c" : "",
      options.promptVersions?.length ? "v" : "",
      options.responseModels?.length ? "m" : "",
      options.operations?.length ? "o" : "",
      options.providers?.length ? "p" : "",
    ].join("");

    const queryFn = this.clickhouse.reader.query({
      name: `promptGenerationsList${filterKey}`,
      query: `SELECT
          run_id, span_id, operation_id, task_identifier, response_model,
          prompt_version,
          input_tokens, output_tokens, total_cost,
          duration / 1000000 AS duration_ms,
          formatDateTime(start_time, '%Y-%m-%d %H:%i:%S') AS started_at
        FROM trigger_dev.llm_metrics_v1
        WHERE environment_id = {environmentId: String}
          AND prompt_slug = {promptSlug: String}
          ${versionClause}
          AND start_time >= parseDateTimeBestEffort({startTime: String})
          AND start_time <= parseDateTimeBestEffort({endTime: String})
          ${cursorClause}
          ${modelClause}
          ${operationClause}
          ${providerClause}
        ORDER BY start_time DESC, span_id DESC
        LIMIT {fetchLimit: UInt32}`,
      params: z.object({
        environmentId: z.string(),
        promptSlug: z.string(),
        startTime: z.string(),
        endTime: z.string(),
        fetchLimit: z.number(),
        ...(decodedCursor
          ? {
              cursorStartTime: z.string(),
              cursorSpanId: z.string(),
            }
          : {}),
        ...(options.promptVersions?.length ? { promptVersions: z.array(z.number()) } : {}),
        ...(options.responseModels?.length ? { responseModels: z.array(z.string()) } : {}),
        ...(options.operations?.length ? { operations: z.array(z.string()) } : {}),
        ...(options.providers?.length ? { providers: z.array(z.string()) } : {}),
      }),
      schema: GenerationRowSchema,
    });

    const queryParams: Record<string, unknown> = {
      environmentId: options.environmentId,
      promptSlug: options.promptSlug,
      startTime: options.startTime.toISOString(),
      endTime: options.endTime.toISOString(),
      fetchLimit: pageSize + 1,
    };

    if (decodedCursor) {
      queryParams.cursorStartTime = decodedCursor.startTime;
      queryParams.cursorSpanId = decodedCursor.spanId;
    }
    if (options.promptVersions?.length) {
      queryParams.promptVersions = options.promptVersions;
    }
    if (options.responseModels?.length) {
      queryParams.responseModels = options.responseModels;
    }
    if (options.operations?.length) {
      queryParams.operations = options.operations;
    }
    if (options.providers?.length) {
      queryParams.providers = options.providers;
    }

    const [error, rows] = await queryFn(queryParams as any);

    if (error) {
      console.error("Prompt generations query failed:", error);
      return { generations: [], pagination: {} };
    }

    const hasMore = rows.length > pageSize;
    const pageRows = hasMore ? rows.slice(0, pageSize) : rows;

    const generations: GenerationRow[] = pageRows.map((r) => ({
      ...r,
      start_time: r.started_at,
    }));

    const pagination: GenerationsPagination = {};
    if (hasMore) {
      const lastRow = pageRows[pageRows.length - 1];
      pagination.next = encodeCursor(lastRow.started_at, lastRow.span_id);
    }

    return { generations, pagination };
  }

  async getDistinctPromptSlugs(
    organizationId: string,
    projectId: string,
    environmentId: string
  ): Promise<string[]> {
    const queryFn = this.clickhouse.reader.query({
      name: "getDistinctPromptSlugs",
      query: `SELECT DISTINCT prompt_slug FROM trigger_dev.llm_metrics_v1 WHERE organization_id = {organizationId: String} AND project_id = {projectId: String} AND environment_id = {environmentId: String} AND prompt_slug != '' ORDER BY prompt_slug`,
      params: z.object({
        organizationId: z.string(),
        projectId: z.string(),
        environmentId: z.string(),
      }),
      schema: z.object({ prompt_slug: z.string() }),
    });

    const [error, rows] = await queryFn({ organizationId, projectId, environmentId });
    if (error) {
      return [];
    }
    return rows.map((r) => r.prompt_slug);
  }

  async getDistinctOperations(
    organizationId: string,
    projectId: string,
    environmentId: string
  ): Promise<string[]> {
    const queryFn = this.clickhouse.reader.query({
      name: "getDistinctOperations",
      query: `SELECT DISTINCT operation_id FROM trigger_dev.llm_metrics_v1 WHERE organization_id = {organizationId: String} AND project_id = {projectId: String} AND environment_id = {environmentId: String} AND operation_id != '' ORDER BY operation_id`,
      params: z.object({
        organizationId: z.string(),
        projectId: z.string(),
        environmentId: z.string(),
      }),
      schema: z.object({ operation_id: z.string() }),
    });

    const [error, rows] = await queryFn({ organizationId, projectId, environmentId });
    if (error) {
      return [];
    }
    return rows.map((r) => r.operation_id);
  }

  async getDistinctProviders(
    organizationId: string,
    projectId: string,
    environmentId: string
  ): Promise<string[]> {
    const queryFn = this.clickhouse.reader.query({
      name: "getDistinctProviders",
      query: `SELECT DISTINCT gen_ai_system FROM trigger_dev.llm_metrics_v1 WHERE organization_id = {organizationId: String} AND project_id = {projectId: String} AND environment_id = {environmentId: String} AND gen_ai_system != '' ORDER BY gen_ai_system`,
      params: z.object({
        organizationId: z.string(),
        projectId: z.string(),
        environmentId: z.string(),
      }),
      schema: z.object({ gen_ai_system: z.string() }),
    });

    const [error, rows] = await queryFn({ organizationId, projectId, environmentId });
    if (error) {
      return [];
    }
    return rows.map((r) => r.gen_ai_system);
  }
}

function encodeCursor(startTime: string, spanId: string): string {
  return Buffer.from(JSON.stringify({ s: startTime, i: spanId })).toString("base64");
}

function decodeCursor(cursor: string): { startTime: string; spanId: string } | null {
  try {
    const parsed = JSON.parse(Buffer.from(cursor, "base64").toString("utf-8")) as Record<
      string,
      unknown
    >;
    if (typeof parsed.s === "string" && typeof parsed.i === "string") {
      return { startTime: parsed.s, spanId: parsed.i };
    }
    return null;
  } catch {
    return null;
  }
}
