import {
  approveSystemAnnotationUseCase,
  deleteAnnotationUseCase,
  listTraceAnnotationsUseCase,
  rejectSystemAnnotationUseCase,
} from "@domain/annotations"
import { writeDraftAnnotationUseCase } from "@domain/annotations/src/use-cases/write-draft-annotation.ts"
import { WorkflowStarter } from "@domain/queue"
import type { AnnotationScore, ScoreListPage } from "@domain/scores"
import { annotationAnchorSchema, ScoreRepository, scoreDraftModeSchema } from "@domain/scores"
import { ProjectId, ScoreId, TraceId } from "@domain/shared"
import { withAi } from "@platform/ai"
import { AIEmbedLive } from "@platform/ai-voyage"
import {
  ScoreAnalyticsRepositoryLive,
  SpanRepositoryLive,
  TraceRepositoryLive,
  withClickHouse,
} from "@platform/db-clickhouse"
import { OutboxEventWriterLive, ScoreRepositoryLive, withPostgres } from "@platform/db-postgres"
import { QueuePublisherLive } from "@platform/queue-bullmq"
import { withTracing } from "@repo/observability"
import { createServerFn } from "@tanstack/react-start"
import { Effect, Layer } from "effect"
import { z } from "zod"
import { requireSession } from "../../server/auth.ts"
import {
  getClickhouseClient,
  getPostgresClient,
  getQueuePublisher,
  getRedisClient,
  getWorkflowStarter,
} from "../../server/clients.ts"

const toRecord = (score: AnnotationScore) => ({
  id: score.id as string,
  organizationId: score.organizationId,
  projectId: score.projectId,
  sessionId: score.sessionId,
  traceId: score.traceId,
  spanId: score.spanId,
  source: score.source,
  sourceId: score.sourceId,
  simulationId: score.simulationId,
  issueId: score.issueId,
  value: score.value,
  passed: score.passed,
  feedback: score.feedback,
  metadata: score.metadata,
  error: score.error,
  errored: score.errored,
  duration: score.duration,
  tokens: score.tokens,
  cost: score.cost,
  draftedAt: score.draftedAt ? score.draftedAt.toISOString() : null,
  annotatorId: score.annotatorId,
  createdAt: score.createdAt.toISOString(),
  updatedAt: score.updatedAt.toISOString(),
})
export type AnnotationRecord = ReturnType<typeof toRecord>

export function isDraftAnnotation(annotation: AnnotationRecord): boolean {
  return annotation.draftedAt !== null
}

const toListResult = (page: ScoreListPage) => ({
  items: page.items.map((s) => toRecord(s as AnnotationScore)),
  hasMore: page.hasMore,
  limit: page.limit,
  offset: page.offset,
})

type AnnotationListResult = ReturnType<typeof toListResult>

const toCountsRecord = (counts: {
  readonly traceId: TraceId
  readonly positiveCount: number
  readonly negativeCount: number
}) => ({
  traceId: counts.traceId as string,
  positiveCount: counts.positiveCount,
  negativeCount: counts.negativeCount,
})

export type TraceAnnotationCountsRecord = ReturnType<typeof toCountsRecord>

/** Annotations created from a queue must pass queue.id as sourceId; otherwise defaults to "UI". */
const getSourceId = (queueId: string | undefined) => queueId ?? "UI"

export const createAnnotation = createServerFn({ method: "POST" })
  .inputValidator(
    z.object({
      projectId: z.string(),
      traceId: z.string().length(32),
      spanId: z.string().optional(),
      sessionId: z.string().optional(),
      queueId: z.string().optional(),
      value: z.number(),
      passed: z.boolean(),
      feedback: z.string().min(1),
      anchor: annotationAnchorSchema.optional(),
      issueId: z.string().optional(),
    }),
  )
  .handler(async ({ data }): Promise<AnnotationRecord> => {
    const { userId, organizationId } = await requireSession()
    const client = getPostgresClient()
    const chClient = getClickhouseClient()
    const publisher = await getQueuePublisher()

    const repositoriesLayer = Layer.mergeAll(ScoreRepositoryLive, OutboxEventWriterLive, QueuePublisherLive(publisher))

    const score = await Effect.runPromise(
      writeDraftAnnotationUseCase({
        organizationId: organizationId,
        projectId: ProjectId(data.projectId),
        sourceId: getSourceId(data.queueId),
        traceId: data.traceId,
        spanId: data.spanId ?? null,
        sessionId: data.sessionId ?? null,
        issueId: data.issueId ?? null,
        annotatorId: userId,
        value: data.value,
        passed: data.passed,
        feedback: data.feedback,
        anchor: data.anchor,
      }).pipe(
        withPostgres(repositoriesLayer, client, organizationId),
        withClickHouse(
          Layer.mergeAll(ScoreAnalyticsRepositoryLive, TraceRepositoryLive, SpanRepositoryLive),
          chClient,
          organizationId,
        ),
        withTracing,
      ),
    )

    return toRecord(score)
  })

export const updateAnnotation = createServerFn({ method: "POST" })
  .inputValidator(
    z.object({
      scoreId: z.string(),
      projectId: z.string(),
      traceId: z.string().length(32),
      queueId: z.string().optional(),
      value: z.number(),
      passed: z.boolean(),
      feedback: z.string().min(1),
      issueId: z.string().optional(),
    }),
  )
  .handler(async ({ data }): Promise<AnnotationRecord> => {
    const { organizationId } = await requireSession()
    const client = getPostgresClient()
    const chClient = getClickhouseClient()
    const publisher = await getQueuePublisher()

    const repositoriesLayer = Layer.mergeAll(ScoreRepositoryLive, OutboxEventWriterLive, QueuePublisherLive(publisher))

    const score = await Effect.runPromise(
      writeDraftAnnotationUseCase({
        organizationId: organizationId,
        id: ScoreId(data.scoreId),
        projectId: ProjectId(data.projectId),
        sourceId: getSourceId(data.queueId),
        traceId: data.traceId,
        issueId: data.issueId ?? null,
        value: data.value,
        passed: data.passed,
        feedback: data.feedback,
      }).pipe(
        withPostgres(repositoriesLayer, client, organizationId),
        withClickHouse(
          Layer.mergeAll(ScoreAnalyticsRepositoryLive, TraceRepositoryLive, SpanRepositoryLive),
          chClient,
          organizationId,
        ),
        withAi(AIEmbedLive, getRedisClient()),
        withTracing,
      ),
    )

    return toRecord(score)
  })

export const deleteAnnotation = createServerFn({ method: "POST" })
  .inputValidator(z.object({ scoreId: z.string(), projectId: z.string() }))
  .handler(async ({ data }): Promise<void> => {
    const { organizationId } = await requireSession()
    const pgClient = getPostgresClient()

    const postgresLayer = Layer.mergeAll(ScoreRepositoryLive, OutboxEventWriterLive)

    await Effect.runPromise(
      deleteAnnotationUseCase({ scoreId: ScoreId(data.scoreId) }).pipe(
        withPostgres(postgresLayer, pgClient, organizationId),
        withTracing,
      ),
    )
  })

export const listAnnotationsByTrace = createServerFn({ method: "GET" })
  .inputValidator(
    z.object({
      projectId: z.string(),
      traceId: z.string(),
      limit: z.number().optional(),
      offset: z.number().optional(),
      draftMode: scoreDraftModeSchema.optional(),
    }),
  )
  .handler(async ({ data }): Promise<AnnotationListResult> => {
    const { organizationId } = await requireSession()
    const client = getPostgresClient()

    const result = await Effect.runPromise(
      listTraceAnnotationsUseCase({
        projectId: data.projectId,
        traceId: data.traceId,
        limit: data.limit,
        offset: data.offset,
        draftMode: data.draftMode ?? "include", // draft-aware by default for trace-scoped reads
      }).pipe(withPostgres(ScoreRepositoryLive, client, organizationId), withTracing),
    )

    return toListResult(result)
  })

export const listAnnotationCountsByTraceIds = createServerFn({ method: "GET" })
  .inputValidator(
    z.object({
      projectId: z.string(),
      traceIds: z.array(z.string().length(32)).max(100),
      draftMode: scoreDraftModeSchema.optional(),
    }),
  )
  .handler(async ({ data }): Promise<readonly TraceAnnotationCountsRecord[]> => {
    if (data.traceIds.length === 0) return []

    const { organizationId } = await requireSession()
    const client = getPostgresClient()

    const result = await Effect.runPromise(
      Effect.gen(function* () {
        const repo = yield* ScoreRepository
        return yield* repo.countAnnotationsByTraceIds({
          projectId: ProjectId(data.projectId),
          traceIds: data.traceIds.map(TraceId),
          options: { draftMode: data.draftMode ?? "include" },
        })
      }).pipe(withPostgres(ScoreRepositoryLive, client, organizationId), withTracing),
    )

    return result.map(toCountsRecord)
  })

export const approveSystemAnnotation = createServerFn({ method: "POST" })
  .inputValidator(z.object({ scoreId: z.string(), comment: z.string().optional() }))
  .handler(async ({ data }): Promise<{ action: "approved" | "already-published" }> => {
    const { organizationId } = await requireSession()
    const client = getPostgresClient()
    const workflowStarter = await getWorkflowStarter()
    const publisher = await getQueuePublisher()

    const result = await Effect.runPromise(
      approveSystemAnnotationUseCase({
        scoreId: ScoreId(data.scoreId),
        ...(data.comment !== undefined ? { comment: data.comment } : {}),
      }).pipe(
        Effect.provide(Layer.mergeAll(Layer.succeed(WorkflowStarter, workflowStarter), QueuePublisherLive(publisher))),
        withPostgres(ScoreRepositoryLive, client, organizationId),
        withTracing,
      ),
    )

    return { action: result.action }
  })

export const rejectSystemAnnotation = createServerFn({ method: "POST" })
  .inputValidator(z.object({ scoreId: z.string(), comment: z.string().min(1) }))
  .handler(async ({ data }): Promise<{ action: "rejected" }> => {
    const { organizationId } = await requireSession()
    const pgClient = getPostgresClient()
    const publisher = await getQueuePublisher()

    const postgresLayer = Layer.mergeAll(ScoreRepositoryLive, OutboxEventWriterLive)

    await Effect.runPromise(
      rejectSystemAnnotationUseCase({ scoreId: ScoreId(data.scoreId), comment: data.comment }).pipe(
        Effect.provide(QueuePublisherLive(publisher)),
        withPostgres(postgresLayer, pgClient, organizationId),
        withTracing,
      ),
    )

    return { action: "rejected" }
  })
