import { Injectable, NotFoundException, ConflictException, BadRequestException, Logger } from '@nestjs/common';
import { PrismaService } from '../../../infrastructure/database/prisma.service';
import { UserAiService } from '../user-ai.service';
import { SnapshotBuilderService, SOURCE_DATA_VERSION } from '../snapshot-builder.service';
import { isValidQuestionType } from '../../quiz/quiz-type.contract';

@Injectable()
export class RuntimeInternalService {
  private readonly logger = new Logger(RuntimeInternalService.name);

  /** Counters for fire-and-forget failures — exposed for health checks and alerting */
  static persistResultFailures = 0;
  static notifyFailures = 0;

  constructor(
    private readonly prisma: PrismaService,
    private readonly userAi: UserAiService,
    private readonly snapshotBuilder: SnapshotBuilderService,
  ) {}

  // ── Poll ──

  async pollJobs(runtimeInstanceId: string, supportedJobTypes: string[], limit: number, capabilities?: Record<string, unknown>) {
    const supSnapshot = (capabilities?.['supportedSnapshotVersions'] as string[]) ?? [];
    const supOutput = (capabilities?.['supportedOutputSchemaVersions'] as string[]) ?? [];

    const where: any = {
      status: 'pending',
      jobType: { in: supportedJobTypes },
    };

    // Filter by outputSchemaVersion if Runtime declared its capabilities
    if (supOutput.length > 0) {
      where.outputSchemaVersion = { in: supOutput };
    }

    const jobs = await this.prisma.aiRuntimeJob.findMany({
      where,
      orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }],
      take: Math.min(limit || 5, 50),
      select: {
        id: true, jobType: true, targetType: true, targetId: true,
        priority: true, snapshotId: true, promptVersion: true, outputSchemaVersion: true,
        apiKeyMode: true, credentialId: true,
      },
    });

    // Register / update RuntimeInstance with capabilities
    if (supSnapshot.length > 0 || supOutput.length > 0) {
      await this.prisma.runtimeInstance.upsert({
        where: { runtimeInstanceId },
        create: {
          runtimeInstanceId,
          status: 'active',
          lastHeartbeatAt: new Date(),
          capabilities: capabilities as any,
        },
        update: {
          status: 'active',
          lastHeartbeatAt: new Date(),
          capabilities: capabilities as any,
        },
      });
    }

    // Post-filter by snapshotVersion if needed (requires snapshot join)
    if (supSnapshot.length > 0) {
      const snapshotIds = [...new Set(jobs.map(j => j.snapshotId).filter(Boolean))];
      if (snapshotIds.length > 0) {
        const snapshots = await this.prisma.learningAnalysisSnapshot.findMany({
          where: { id: { in: snapshotIds as string[] } },
          select: { id: true, snapshotVersion: true },
        });
        const compatibleIds = new Set(
          snapshots.filter(s => supSnapshot.includes(s.snapshotVersion)).map(s => s.id)
        );
        return { jobs: jobs.filter(j => !j.snapshotId || compatibleIds.has(j.snapshotId)) };
      }
    }

    return { jobs };
  }

  // ── Lock ──

  async lockJob(jobId: string, runtimeInstanceId: string) {
    const now = new Date();
    const lockUntil = new Date(now.getTime() + 60_000);

    const result = await this.prisma.aiRuntimeJob.updateMany({
      where: {
        id: jobId,
        status: 'pending',
        OR: [
          { lockUntil: null },
          { lockUntil: { lt: now } },
        ],
      },
      data: {
        status: 'locked',
        lockedBy: runtimeInstanceId,
        lockedAt: now,
        lockUntil,
      },
    });

    if (result.count === 0) {
      throw new ConflictException({
        errorCode: 'JOB_ALREADY_LOCKED',
        message: 'Job is already locked or not in pending status',
      });
    }

    return { jobId, status: 'locked', lockUntil: lockUntil.getTime() };
  }

  // ── Heartbeat ──

  async heartbeatJob(jobId: string, runtimeInstanceId: string) {
    const now = new Date();
    const lockUntil = new Date(now.getTime() + 60_000);

    // locked → running transition: set startedAt once
    const lockedResult = await this.prisma.aiRuntimeJob.updateMany({
      where: { id: jobId, lockedBy: runtimeInstanceId, status: 'locked' },
      data: { lockUntil, status: 'running', startedAt: now },
    });

    // running → running: only extend lockUntil, preserve original startedAt
    const runningResult = await this.prisma.aiRuntimeJob.updateMany({
      where: { id: jobId, lockedBy: runtimeInstanceId, status: 'running' },
      data: { lockUntil },
    });

    if (lockedResult.count === 0 && runningResult.count === 0) {
      throw new NotFoundException({ errorCode: 'JOB_NOT_FOUND', message: 'Job not found or not locked by this runtime' });
    }

    // Check if cancellation was requested
    const job = await this.prisma.aiRuntimeJob.findUnique({
      where: { id: jobId },
      select: { cancelRequestedAt: true },
    });

    return {
      jobId,
      lockUntil: lockUntil.getTime(),
      cancelRequested: job?.cancelRequestedAt != null,
    };
  }

  // ── Snapshot ──

  async getSnapshot(jobId: string) {
    const job = await this.prisma.aiRuntimeJob.findUnique({
      where: { id: jobId },
      select: { id: true, userId: true, targetType: true, targetId: true, snapshotId: true },
    });
    if (!job) throw new NotFoundException({ errorCode: 'JOB_NOT_FOUND', message: 'Job not found' });

    const snapshot = await this.resolveSnapshot(job);
    return {
      jobId: job.id,
      snapshotId: snapshot.id,
      snapshotVersion: snapshot.snapshotVersion,
      privacyScope: snapshot.privacyScope,
      userProfile: snapshot.userProfile,
      aiSettings: snapshot.aiSettings,
      deviceContext: snapshot.deviceContext,
      learningBehaviorSummary: snapshot.learningBehaviorSummary,
      materialProgressSummary: snapshot.materialProgressSummary,
      contentStructureSummary: snapshot.contentStructureSummary,
      behaviorSignals: snapshot.behaviorSignals,
      scoreSignals: snapshot.scoreSignals,
      constraints: snapshot.constraints,
      allowedModelFields: snapshot.allowedModelFields,
    };
  }

  private async resolveSnapshot(job: { id: string; userId: string; targetType: string; targetId: string; snapshotId: string | null }) {
    if (job.snapshotId) {
      const existing = await this.prisma.learningAnalysisSnapshot.findUnique({
        where: { id: job.snapshotId },
      });
      if (existing
        && existing.sourceDataVersion === SOURCE_DATA_VERSION
        && (!existing.expiresAt || new Date(existing.expiresAt) >= new Date())) {
        return existing;
      }
    }

    const snapshot = await this.snapshotBuilder.buildSnapshot(job.userId, job.targetType, job.targetId);
    await this.prisma.aiRuntimeJob.update({
      where: { id: job.id },
      data: { snapshotId: snapshot.id },
    });
    return snapshot;
  }

  // ── Credential Resolve ──

  async resolveCredential(jobId: string, apiKeyMode: string, provider: string, credentialId?: string) {
    const job = await this.prisma.aiRuntimeJob.findUnique({
      where: { id: jobId },
      select: { userId: true },
    });
    if (!job) throw new NotFoundException({ errorCode: 'JOB_NOT_FOUND', message: 'Job not found' });

    if (apiKeyMode === 'user_deepseek_key') {
      if (!credentialId) throw new BadRequestException({ errorCode: 'CREDENTIAL_NOT_FOUND', message: 'credentialId required for user_deepseek_key mode' });
      const { provider: resolvedProvider, apiKey } = await this.userAi.resolveCredentialForJob(job.userId, credentialId);
      return { provider: resolvedProvider, model: 'deepseek-chat', apiKey, apiKeyMode: 'user_deepseek_key' };
    }

    // platform_key: Runtime should use its own env var as primary; API returns empty key as signal
    return { provider, model: 'deepseek-chat', apiKey: '', apiKeyMode: 'platform_key' };
  }

  // ── Result ──

  async submitResult(jobId: string, dto: {
    runtimeInstanceId: string; schemaVersion: string; status: string;
    rawOutput?: any; validatedOutput?: any; validationErrors?: string[];
    usage?: any; attemptNo: number; outputHash?: string;
  }) {
    const job = await this.prisma.aiRuntimeJob.findUnique({ where: { id: jobId } });
    if (!job) throw new NotFoundException({ errorCode: 'JOB_NOT_FOUND', message: 'Job not found' });
    if (job.status !== 'locked' && job.status !== 'running') {
      throw new ConflictException({ errorCode: 'JOB_NOT_ACTIVE', message: `Job is ${job.status}, cannot accept result` });
    }

    // Validate schema version compatibility
    if (job.outputSchemaVersion && dto.schemaVersion !== job.outputSchemaVersion) {
      throw new BadRequestException({
        errorCode: 'RESULT_SCHEMA_UNSUPPORTED',
        message: `Result schemaVersion ${dto.schemaVersion} does not match job outputSchemaVersion ${job.outputSchemaVersion}`,
      });
    }

    const resultIdempotencyKey = `${jobId}:${dto.attemptNo}:${dto.outputHash ?? ''}`;

    // Check duplicate
    const existing = await this.prisma.aiRuntimeResult.findFirst({
      where: { resultIdempotencyKey },
    });
    if (existing) return { status: 'ok', duplicate: true };

    // Check already succeeded with different hash
    const existingResult = await this.prisma.aiRuntimeResult.findUnique({ where: { jobId } });
    if (existingResult && existingResult.status === 'succeeded') {
      throw new ConflictException({ errorCode: 'RESULT_ALREADY_EXISTS', message: 'Job already has a succeeded result' });
    }

    await this.prisma.aiRuntimeResult.create({
      data: {
        jobId, userId: job.userId,
        runtimeInstanceId: dto.runtimeInstanceId,
        status: dto.status,
        attemptNo: dto.attemptNo,
        resultIdempotencyKey,
        outputHash: dto.outputHash,
        rawOutput: dto.rawOutput as any,
        validatedOutput: dto.validatedOutput as any,
        schemaVersion: dto.schemaVersion,
        validationErrors: dto.validationErrors as any,
      },
    });

    await this.prisma.aiRuntimeJob.update({
      where: { id: jobId },
      data: { status: 'succeeded', finishedAt: new Date() },
    });

    await this.persistResult(job, dto).catch(err => {
      RuntimeInternalService.persistResultFailures++;
      this.logger.error(`Result persistence failed for job=${jobId}: ${err.message}`, err.stack);
    });

    this.notifyJobComplete(job.userId, jobId, job.jobType, 'succeeded').catch(err => {
      RuntimeInternalService.notifyFailures++;
      this.logger.error(`Notification failed for job=${jobId}: ${err.message}`, err.stack);
    });

    return { status: 'ok', duplicate: false };
  }

  private async persistResult(job: { id: string; userId: string; jobType: string; targetType: string; targetId: string; snapshotId: string | null; promptVersion: string | null; outputSchemaVersion: string | null }, dto: { validatedOutput?: any }) {
    const output = dto.validatedOutput;
    if (!output) return;

    const errors = this.validateOutput(job.jobType, output);
    if (errors.length > 0) {
      this.logger.warn(`Output validation warnings for job=${job.id} type=${job.jobType}: ${errors.join('; ')}`);
    }

    if (job.jobType === 'learning_state_analysis') {
      await this.prisma.aiLearningAnalysis.create({
        data: {
          userId: job.userId, jobId: job.id,
          snapshotId: job.snapshotId,
          targetType: job.targetType, targetId: job.targetId,
          learningState: output.learningState ?? null,
          summary: output.summary ?? null,
          riskLevel: output.riskLevel ?? null,
          confidence: output.confidence ?? null,
          evidence: output.evidence ?? undefined,
          nextActionIds: output.nextActionIds ?? undefined,
          promptVersion: job.promptVersion,
          schemaVersion: job.outputSchemaVersion,
        },
      });
    }

    if (job.jobType === 'weak_point_analysis') {
      // Resolve previous active weak points for same target to avoid duplicate accumulation
      if (job.targetId) {
        await this.prisma.weakPointCandidate.updateMany({
          where: { userId: job.userId, targetType: job.targetType, targetId: job.targetId, status: 'active' },
          data: { status: 'resolved' },
        });
      }
      const candidates = Array.isArray(output.candidates) ? output.candidates : (output.candidates ? [output.candidates] : []);
      for (const wp of candidates) {
        await this.prisma.weakPointCandidate.create({
          data: {
            userId: job.userId, jobId: job.id,
            snapshotId: job.snapshotId,
            targetType: job.targetType, targetId: job.targetId,
            knowledgePointId: wp.knowledgePointId ?? null,
            title: wp.title ?? 'Untitled Weak Point',
            reason: wp.reason ?? null,
            confidence: wp.confidence ?? null,
            evidence: wp.evidence ?? undefined,
            status: 'active',
          },
        });
      }
    }

    if (job.jobType === 'next_action_planning') {
      // Resolve previous active recommendations for same target
      if (job.targetId) {
        await this.prisma.nextActionRecommendation.updateMany({
          where: { userId: job.userId, targetType: job.targetType ?? undefined, targetId: job.targetId ?? undefined, status: 'active' },
          data: { status: 'resolved' },
        });
      }
      const actions = Array.isArray(output.actions) ? output.actions : (output.actions ? [output.actions] : []);
      for (const action of actions) {
        await this.prisma.nextActionRecommendation.create({
          data: {
            userId: job.userId, jobId: job.id,
            snapshotId: job.snapshotId,
            actionType: action.actionType ?? 'general',
            targetType: action.targetType ?? null,
            targetId: action.targetId ?? null,
            title: action.title ?? 'Untitled Action',
            reason: action.reason ?? null,
            priority: action.priority ?? 0,
            estimatedMinutes: action.estimatedMinutes ?? null,
            deviceSuitability: action.deviceSuitability ?? null,
            status: 'active',
          },
        });
      }
    }

    if (job.jobType === 'quiz_generation') {
      const questions = Array.isArray(output.questions) ? output.questions : (output.questions ? [output.questions] : []);
      const questionCount = questions.length || output.count || 0;

      await this.prisma.questionGenerationPlan.updateMany({
        where: { jobId: job.id },
        data: {
          status: 'completed',
          reason: output.summary ?? `Generated ${questionCount} question(s)`,
        },
      });

      if (questions.length > 0 && job.targetType === 'knowledge_base') {
        await this.convertQuizCandidates(job, output, questions);
      }
    }

    if (job.jobType === 'flashcard_generation') {
      const cards = Array.isArray(output.cards) ? output.cards : (output.cards ? [output.cards] : []);
      const cardCount = cards.length || output.count || 0;

      await this.prisma.flashcardGenerationPlan.updateMany({
        where: { jobId: job.id },
        data: {
          status: 'completed',
          reason: output.summary ?? `Generated ${cardCount} card(s)`,
        },
      });

      if (cards.length > 0) {
        await this.convertFlashcardCandidates(job, cards);
      }
    }
  }

  // ── Output Validation ──

  private validateOutput(jobType: string, output: any): string[] {
    const warnings: string[] = [];

    if (jobType === 'learning_state_analysis') {
      if (!output.learningState) warnings.push('missing learningState');
      if (output.confidence != null && (output.confidence < 0 || output.confidence > 1)) {
        warnings.push('confidence out of range [0,1]');
      }
    }

    if (jobType === 'weak_point_analysis') {
      const candidates = Array.isArray(output.candidates) ? output.candidates : (output.candidates ? [output.candidates] : []);
      if (candidates.length === 0) warnings.push('no candidates in output');
      for (const wp of candidates) {
        if (!wp.title && !wp.knowledgePointId) warnings.push('candidate missing title and knowledgePointId');
      }
    }

    if (jobType === 'next_action_planning') {
      const actions = Array.isArray(output.actions) ? output.actions : (output.actions ? [output.actions] : []);
      if (actions.length === 0) warnings.push('no actions in output');
      for (const a of actions) {
        if (!a.title) warnings.push('action missing title');
      }
    }

    return warnings;
  }

  // ── Quiz Candidate Conversion ──

  private async convertQuizCandidates(
    job: { userId: string; id: string; targetId: string },
    output: { quizTitle?: string; quizDescription?: string },
    questions: any[],
  ) {
    const valid = questions.filter(q => (q.stem || q.question) && (q.answer || q.correctAnswer));
    const skipped = questions.length - valid.length;
    if (skipped > 0) {
      this.logger.warn(`convertQuizCandidates: skipped ${skipped} question(s) missing stem or answer for job=${job.id}`);
    }
    if (valid.length === 0) return;

    const quiz = await this.prisma.quiz.create({
      data: {
        userId: job.userId,
        knowledgeBaseId: job.targetId,
        title: output.quizTitle ?? 'AI Generated Quiz',
        description: output.quizDescription ?? null,
        questionCount: valid.length,
        sourceType: 'ai',
        sourceId: job.id,
        status: 'ready',
      },
    });

    let created = 0;

    // Batch pre-fetch existing stems for dedup (avoids N+1 per-item queries)
    const stems = valid.map(q => q.stem ?? q.question);
    const existingStems = new Set(
      (await this.prisma.quizQuestion.findMany({
        where: {
          stem: { in: stems },
          quiz: { userId: job.userId },
        },
        select: { stem: true },
      })).map(r => r.stem),
    );

    for (let i = 0; i < valid.length; i++) {
      const q = valid[i];
      const stem = stems[i];

      if (existingStems.has(stem)) {
        this.logger.warn(`convertQuizCandidates: skipping duplicate question stem="${stem.substring(0, 50)}"`);
        continue;
      }

      // M-QUIZ-01-CLEANUP-01: 缺失 type → 跳过 + 警告（兼容旧数据）；非法非空 type → 拒绝
      if (!q.type) {
        this.logger.warn(`convertQuizCandidates: skipping question with missing type for job=${job.id}`);
        continue;
      }
      if (!isValidQuestionType(q.type)) {
        throw new BadRequestException(`Invalid question type: ${q.type}`);
      }

      await this.prisma.quizQuestion.create({
        data: {
          quizId: quiz.id,
          type: q.type,
          stem,
          options: q.options ?? undefined,
          answer: q.answer ?? q.correctAnswer,
          explanation: q.explanation ?? null,
          sourceBlockIds: Array.isArray(q.sourceBlockIds) ? q.sourceBlockIds : undefined,
          orderIndex: created,
        },
      });
      created++;
    }

    if (created < valid.length) {
      await this.prisma.quiz.update({
        where: { id: quiz.id },
        data: { questionCount: created },
      });
    }
  }

  // ── Flashcard Candidate Conversion ──

  private async convertFlashcardCandidates(
    job: { userId: string; id: string; targetId: string; promptVersion: string | null; outputSchemaVersion: string | null },
    cards: any[],
  ) {
    const valid = cards.filter(c => (c.front || c.question) && (c.back || c.answer));
    const skipped = cards.length - valid.length;
    if (skipped > 0) {
      this.logger.warn(`convertFlashcardCandidates: skipped ${skipped} card(s) missing front/back for job=${job.id}`);
    }

    // Validate sourceBlockIds: resolve referenced knowledge items exist
    const allBlockIds = [...new Set(valid.flatMap(c => (Array.isArray(c.sourceBlockIds) ? c.sourceBlockIds : [])))];
    const existingBlockIds = allBlockIds.length > 0
      ? new Set(
          (await this.prisma.knowledgeItem.findMany({
            where: { id: { in: allBlockIds }, userId: job.userId },
            select: { id: true },
          })).map(k => k.id),
        )
      : new Set<string>();

    // Batch pre-fetch existing fronts for dedup (avoids N+1 per-item queries)
    const fronts = valid.map(c => c.front ?? c.question);
    const existingFronts = new Set(
      (await this.prisma.flashcard.findMany({
        where: { userId: job.userId, front: { in: fronts }, deletedAt: null },
        select: { front: true },
      })).map(r => r.front),
    );

    for (const card of valid) {
      const rawIds: string[] = Array.isArray(card.sourceBlockIds) ? card.sourceBlockIds : [];
      const validIds = rawIds.filter(id => existingBlockIds.has(id));
      if (rawIds.length > 0 && validIds.length < rawIds.length) {
        this.logger.warn(`convertFlashcardCandidates: filtered ${rawIds.length - validIds.length} invalid sourceBlockIds`);
      }

      const front = card.front ?? card.question;
      if (existingFronts.has(front)) {
        this.logger.warn(`convertFlashcardCandidates: skipping duplicate front="${front.substring(0, 50)}"`);
        continue;
      }
      await this.prisma.flashcard.create({
        data: {
          userId: job.userId,
          sourceType: 'ai',
          sourceId: job.id,
          knowledgePointId: card.knowledgePointId ?? null,
          front: card.front ?? card.question,
          back: card.back ?? card.answer,
          hint: card.hint ?? null,
          difficultyLevel: card.difficultyLevel ?? null,
          sourceBlockIds: validIds.length > 0 ? validIds : undefined,
          generatedByJobId: job.id,
          promptVersion: job.promptVersion,
          schemaVersion: job.outputSchemaVersion,
          status: 'draft',
        },
      });
    }
  }

  // ── Fail ──

  async submitFailure(jobId: string, dto: {
    runtimeInstanceId: string; errorCode: string; errorMessage: string;
    retryable: boolean; rawError?: string;
  }) {
    const job = await this.prisma.aiRuntimeJob.findUnique({ where: { id: jobId } });
    if (!job) throw new NotFoundException({ errorCode: 'JOB_NOT_FOUND', message: 'Job not found' });
    if (job.status !== 'locked' && job.status !== 'running') {
      throw new ConflictException({ errorCode: 'JOB_NOT_ACTIVE', message: `Job is ${job.status}, cannot accept failure` });
    }

    if (dto.errorCode === 'JOB_CANCELLED') {
      await this.prisma.aiRuntimeJob.update({
        where: { id: jobId },
        data: { status: 'cancelled', cancelledAt: new Date(), finishedAt: new Date() },
      });
      return { status: 'cancelled' };
    }

    const newRetryCount = job.retryCount + 1;
    const exceeded = newRetryCount > job.maxRetryCount;

    if (dto.retryable && !exceeded) {
      await this.prisma.aiRuntimeJob.update({
        where: { id: jobId },
        data: {
          status: 'pending',
          lockedBy: null, lockedAt: null, lockUntil: null,
          retryCount: newRetryCount,
          errorCode: dto.errorCode,
          errorMessage: dto.errorMessage,
        },
      });
    } else {
      await this.prisma.aiRuntimeJob.update({
        where: { id: jobId },
        data: {
          status: 'failed',
          finishedAt: new Date(),
          retryCount: newRetryCount,
          errorCode: dto.errorCode,
          errorMessage: dto.errorMessage,
        },
      });
    }

    if (exceeded) {
      this.notifyJobComplete(job.userId, jobId, job.jobType, 'failed').catch(err => {
        RuntimeInternalService.notifyFailures++;
        this.logger.error(`Notification failed for job=${jobId}: ${err.message}`, err.stack);
      });
    }

    return { status: exceeded ? 'failed' : 'pending', retryCount: newRetryCount };
  }

  // ── Job Completion Notification ──

  private async notifyJobComplete(userId: string, jobId: string, jobType: string, status: string) {
    const title = status === 'succeeded'
      ? `AI analysis complete`
      : `AI analysis failed`;
    const content = status === 'succeeded'
      ? `Your ${jobType} job has completed successfully.`
      : `Your ${jobType} job has failed.`;

    await this.prisma.notification.create({
      data: {
        userId,
        type: status === 'succeeded' ? 'ai_job_succeeded' : 'ai_job_failed',
        title,
        content,
        data: { jobId, jobType, status },
      },
    });
  }

  // ── Invocation Logs ──

  async submitInvocationLogs(logs: Array<{
    jobId: string; provider: string; model: string; apiKeyMode: string;
    credentialId?: string; promptName: string; promptVersion: string;
    outputSchemaVersion: string; inputTokens: number; outputTokens: number;
    totalTokens: number; latencyMs: number; costEstimate?: number;
    success: boolean; errorCode?: string; errorMessage?: string;
    retryCount: number; runtimeInstanceId: string;
    traceId?: string; correlationId?: string;
  }>) {
    const created = await Promise.all(
      logs.map(async (log) => {
        const job = await this.prisma.aiRuntimeJob.findUnique({
          where: { id: log.jobId }, select: { userId: true },
        });
        if (!job) return null;

        return this.prisma.modelInvocationLog.create({
          data: {
            userId: job.userId,
            jobId: log.jobId,
            provider: log.provider,
            model: log.model,
            apiKeyMode: log.apiKeyMode,
            credentialId: log.credentialId,
            promptName: log.promptName,
            promptVersion: log.promptVersion,
            outputSchemaVersion: log.outputSchemaVersion,
            inputTokens: log.inputTokens,
            outputTokens: log.outputTokens,
            totalTokens: log.totalTokens,
            latencyMs: log.latencyMs,
            costEstimate: log.costEstimate,
            success: log.success,
            errorCode: log.errorCode,
            errorMessage: log.errorMessage,
            retryCount: log.retryCount,
            runtimeInstanceId: log.runtimeInstanceId,
            traceId: log.traceId,
            correlationId: log.correlationId,
          },
        });
      }),
    );
    return { accepted: created.filter(Boolean).length };
  }
}
