import { type ClickHouse } from "@internal/clickhouse";
import { type RunStore } from "@internal/run-store";
import { type Tracer } from "@internal/tracing";
import { type Logger, type LogLevel } from "@trigger.dev/core/logger";
import { MachinePresetName } from "@trigger.dev/core/v3";
import { BulkActionId, RunId } from "@trigger.dev/core/v3/isomorphic";
import { type Prisma, TaskRunStatus } from "@trigger.dev/database";
import parseDuration from "parse-duration";
import { z } from "zod";
import { timeFilters } from "~/components/runs/v3/SharedFilters";
import { type PrismaClientOrTransaction } from "~/db.server";
import { runStore as defaultRunStore } from "~/v3/runStore.server";
import { startActiveSpan } from "~/v3/tracer.server";
import { ClickHouseRunsRepository } from "./clickhouseRunsRepository.server";

export type RunsRepositoryOptions = {
  clickhouse: ClickHouse;
  prisma: PrismaClientOrTransaction;
  logger?: Logger;
  logLevel?: LogLevel;
  tracer?: Tracer;

  // Injectable run-ops store; defaults to the `~/v3/runStore.server` singleton
  // (passthrough). The list-hydrate fan-out below does not depend on the store
  // routing mixed-residency id sets — it applies the read-through fan-out itself.
  runStore?: RunStore;

  // Run-ops read-through wiring for the list hydrate. Omitted => passthrough.
  readThrough?: {
    // `legacyReplica` is a READ REPLICA handle only — there is no legacy-primary field.
    newClient?: PrismaClientOrTransaction;
    legacyReplica?: PrismaClientOrTransaction;
    // Resolved boot constant; when false the split branch is never entered.
    splitEnabled?: boolean;
  };
};

const RunStatus = z.enum(Object.values(TaskRunStatus) as [TaskRunStatus, ...TaskRunStatus[]]);

const RunListInputOptionsSchema = z.object({
  organizationId: z.string(),
  projectId: z.string(),
  environmentId: z.string(),
  //filters
  tasks: z.array(z.string()).optional(),
  versions: z.array(z.string()).optional(),
  statuses: z.array(RunStatus).optional(),
  tags: z.array(z.string()).optional(),
  // "any" (default) = run has at least one of `tags`; "all" = run has every tag.
  tagsMatch: z.enum(["any", "all"]).optional(),
  scheduleId: z.string().optional(),
  period: z.string().optional(),
  from: z.number().optional(),
  to: z.number().optional(),
  isTest: z.boolean().optional(),
  rootOnly: z.boolean().optional(),
  batchId: z.string().optional(),
  runId: z.array(z.string()).optional(),
  bulkId: z.string().optional(),
  queues: z.array(z.string()).optional(),
  regions: z.array(z.string()).optional(),
  machines: MachinePresetName.array().optional(),
  errorId: z.string().optional(),
  taskKinds: z.array(z.string()).optional(),
});

export type RunListInputOptions = z.infer<typeof RunListInputOptionsSchema>;
export type RunListInputFilters = Omit<
  RunListInputOptions,
  "organizationId" | "projectId" | "environmentId"
>;

export type ParsedRunFilters = RunListInputFilters & {
  cursor?: string;
  direction?: "forward" | "backward";
  sources?: string[];
};

export type FilterRunsOptions = Omit<RunListInputOptions, "period"> & {
  period: number | undefined;
};

type Pagination = {
  page: {
    size: number;
    cursor?: string;
    direction?: "forward" | "backward";
  };
};

type OffsetPagination = {
  offset: number;
  limit: number;
};

export type ListedRun = Prisma.TaskRunGetPayload<{
  select: {
    id: true;
    friendlyId: true;
    taskIdentifier: true;
    taskVersion: true;
    runtimeEnvironmentId: true;
    status: true;
    createdAt: true;
    startedAt: true;
    lockedAt: true;
    delayUntil: true;
    updatedAt: true;
    completedAt: true;
    isTest: true;
    spanId: true;
    idempotencyKey: true;
    ttl: true;
    expiredAt: true;
    costInCents: true;
    baseCostInCents: true;
    usageDurationMs: true;
    runTags: true;
    depth: true;
    rootTaskRunId: true;
    batchId: true;
    metadata: true;
    metadataType: true;
    machinePreset: true;
    queue: true;
    workerQueue: true;
    region: true;
    annotations: true;
  };
}>;

export type ListRunsOptions = RunListInputOptions & Pagination;

export type TagListOptions = {
  organizationId: string;
  projectId: string;
  environmentId: string;
  period?: string;
  from?: number;
  to?: number;
  /** Performs a case insensitive contains search on the tag name */
  query?: string;
} & OffsetPagination;

export type TagList = {
  tags: string[];
};

export type CursorPagination = {
  nextCursor: string | null;
  previousCursor: string | null;
};

export type RunIdsPage = {
  runIds: string[];
  pagination: CursorPagination;
};

export interface IRunsRepository {
  name: string;
  /**
   * A keyset-paginated page of run ids plus the cursors to navigate
   * forward/backward. The cursors are opaque composite `(created_at, run_id)`
   * tokens, so pagination can't duplicate or skip runs. This is the single
   * cursor-aware list primitive — `listRuns` and bulk actions build on it.
   */
  listRunIds(options: ListRunsOptions): Promise<RunIdsPage>;
  /** Returns friendly IDs (e.g., run_xxx) instead of internal UUIDs. Used for ClickHouse task_events queries. */
  listFriendlyRunIds(options: ListRunsOptions): Promise<string[]>;
  listRuns(options: ListRunsOptions): Promise<{
    runs: ListedRun[];
    pagination: {
      nextCursor: string | null;
      previousCursor: string | null;
    };
  }>;
  countRuns(options: RunListInputOptions): Promise<number>;
  listTags(options: TagListOptions): Promise<TagList>;
  runExistsInEnvironment(options: {
    organizationId: string;
    projectId: string;
    environmentId: string;
    createdAtLowerBoundMs?: number;
  }): Promise<boolean>;
}

export class RunsRepository implements IRunsRepository {
  private readonly clickHouseRunsRepository: ClickHouseRunsRepository;

  constructor(private readonly options: RunsRepositoryOptions) {
    this.clickHouseRunsRepository = new ClickHouseRunsRepository(options);
  }

  get name() {
    return "runsRepository";
  }

  async runExistsInEnvironment(options: {
    organizationId: string;
    projectId: string;
    environmentId: string;
    createdAtLowerBoundMs?: number;
  }): Promise<boolean> {
    return startActiveSpan(
      "runsRepository.runExistsInEnvironment",
      async () => this.clickHouseRunsRepository.runExistsInEnvironment(options),
      {
        attributes: {
          "repository.name": "clickhouse",
          organizationId: options.organizationId,
          projectId: options.projectId,
          environmentId: options.environmentId,
        },
      }
    );
  }

  async listRunIds(options: ListRunsOptions): Promise<RunIdsPage> {
    return startActiveSpan(
      "runsRepository.listRunIds",
      async () => this.clickHouseRunsRepository.listRunIds(options),
      {
        attributes: {
          "repository.name": "clickhouse",
          organizationId: options.organizationId,
          projectId: options.projectId,
          environmentId: options.environmentId,
        },
      }
    );
  }

  async listFriendlyRunIds(options: ListRunsOptions): Promise<string[]> {
    return startActiveSpan(
      "runsRepository.listFriendlyRunIds",
      async () => this.clickHouseRunsRepository.listFriendlyRunIds(options),
      {
        attributes: {
          "repository.name": "clickhouse",
          "readThrough.split": Boolean(this.options.readThrough?.splitEnabled),
          organizationId: options.organizationId,
          projectId: options.projectId,
          environmentId: options.environmentId,
        },
      }
    );
  }

  async listRuns(options: ListRunsOptions): Promise<{
    runs: ListedRun[];
    pagination: {
      nextCursor: string | null;
      previousCursor: string | null;
    };
  }> {
    return startActiveSpan(
      "runsRepository.listRuns",
      async () => this.clickHouseRunsRepository.listRuns(options),
      {
        attributes: {
          "repository.name": "clickhouse",
          "readThrough.split": Boolean(this.options.readThrough?.splitEnabled),
          organizationId: options.organizationId,
          projectId: options.projectId,
          environmentId: options.environmentId,
        },
      }
    );
  }

  async countRuns(options: RunListInputOptions): Promise<number> {
    return startActiveSpan(
      "runsRepository.countRuns",
      async () => this.clickHouseRunsRepository.countRuns(options),
      {
        attributes: {
          "repository.name": "clickhouse",
          organizationId: options.organizationId,
          projectId: options.projectId,
          environmentId: options.environmentId,
        },
      }
    );
  }

  async listTags(options: TagListOptions): Promise<TagList> {
    return startActiveSpan(
      "runsRepository.listTags",
      async () => this.clickHouseRunsRepository.listTags(options),
      {
        attributes: {
          "repository.name": "clickhouse",
          organizationId: options.organizationId,
          projectId: options.projectId,
          environmentId: options.environmentId,
        },
      }
    );
  }
}

export function parseRunListInputOptions(data: any): RunListInputOptions {
  return RunListInputOptionsSchema.parse(data);
}

export async function convertRunListInputOptionsToFilterRunsOptions(
  options: RunListInputOptions,
  prisma: RunsRepositoryOptions["prisma"],
  store: RunStore = defaultRunStore
): Promise<FilterRunsOptions> {
  const convertedOptions: FilterRunsOptions = {
    ...options,
    period: undefined,
  };

  // Convert time period to ms
  const time = timeFilters({
    period: options.period,
    from: options.from,
    to: options.to,
  });
  convertedOptions.period = time.period ? (parseDuration(time.period) ?? undefined) : undefined;

  // Cross-DB resolution: BatchTaskRun is a RUN-OPS table. A run-ops batch resident on the
  // dedicated run-ops DB must resolve via the store's NEW->LEGACY probe — a single control-plane
  // client would miss it and leave the friendlyId in the ClickHouse `batch_id` filter, matching
  // nothing. Split off / self-host: the store is a passthrough over the one client.
  if (options.batchId && options.batchId.startsWith("batch_")) {
    const batch = await store.findBatchTaskRunByFriendlyId(options.batchId, options.environmentId);

    if (batch) {
      convertedOptions.batchId = batch.id;
    }
  }

  // ScheduleId can be a friendlyId. TaskSchedule is a CONTROL-PLANE table, so this stays on
  // the passed `prisma` (the control-plane client) in both single-DB and split modes.
  if (options.scheduleId && options.scheduleId.startsWith("sched_")) {
    const schedule = await prisma.taskSchedule.findFirst({
      select: {
        id: true,
      },
      where: {
        friendlyId: options.scheduleId,
        projectId: options.projectId,
      },
    });

    if (schedule) {
      convertedOptions.scheduleId = schedule?.id;
    }
  }

  if (options.bulkId && options.bulkId.startsWith("bulk_")) {
    convertedOptions.bulkId = BulkActionId.toId(options.bulkId);
  }

  if (options.runId) {
    // Convert to friendlyId
    convertedOptions.runId = options.runId.map((r) => RunId.toFriendlyId(r));
  }

  // batchId/runId/scheduleId target specific runs, so rootOnly is meaningless and forced off.
  // tasks is intentionally excluded so rootOnly can narrow a task filter to root runs only.
  if (options.batchId || options.runId?.length || options.scheduleId) {
    convertedOptions.rootOnly = false;
  }

  return convertedOptions;
}
