import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../../infrastructure/database/prisma.service';
import { CredentialEncryptionService } from './credential-encryption.service';
import { SnapshotBuilderService } from './snapshot-builder.service';
import { PriorityRulesService } from './priority-rules.service';
import { UserAiQuotaService } from './user-ai-quota.service';
import { PlatformBudgetService } from './platform-budget.service';
import type { SaveLearningProfileDto, UpdateAiSettingsDto, CreateCredentialDto, UpdateCredentialDto, CredentialResponseDto, CreateAnalysisJobDto, CreateAnalysisJobResponseDto } from './user-ai.dto';

const JOB_TYPE_CONFIG: Record<string, { promptVersion: string; outputSchemaVersion: string }> = {
  learning_state_analysis: { promptVersion: 'learning_state_v1', outputSchemaVersion: 'analysis_output_v1' },
  weak_point_analysis:    { promptVersion: 'weak_point_v1',    outputSchemaVersion: 'weak_point_output_v1' },
  next_action_planning:   { promptVersion: 'next_action_v1',   outputSchemaVersion: 'next_action_output_v1' },
  quiz_generation:        { promptVersion: 'quiz_gen_v1',      outputSchemaVersion: 'quiz_output_v1' },
  flashcard_generation:   { promptVersion: 'flashcard_gen_v1', outputSchemaVersion: 'flashcard_output_v1' },
};

@Injectable()
export class UserAiService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly crypto: CredentialEncryptionService,
    private readonly snapshotBuilder: SnapshotBuilderService,
    private readonly priorityRules: PriorityRulesService,
    private readonly quota: UserAiQuotaService,
    private readonly budget: PlatformBudgetService,
    private readonly config: ConfigService,
  ) {}

  // ══ LearningProfile ══

  async getProfile(userId: string) {
    return this.prisma.userLearningProfile.findUnique({ where: { userId } });
  }

  async saveProfile(userId: string, dto: SaveLearningProfileDto) {
    return this.prisma.userLearningProfile.upsert({
      where: { userId },
      create: { userId, ...dto },
      update: dto,
    });
  }

  // ══ AiSettings ══

  async getSettings(userId: string) {
    let settings = await this.prisma.userAiSettings.findUnique({ where: { userId } });
    if (!settings) {
      settings = await this.prisma.userAiSettings.create({ data: { userId } });
    }
    return settings;
  }

  async updateSettings(userId: string, dto: UpdateAiSettingsDto) {
    if (dto.apiKeyMode === 'user_deepseek_key' && dto.defaultCredentialId) {
      const cred = await this.prisma.userModelCredential.findFirst({
        where: { id: dto.defaultCredentialId, userId },
      });
      if (!cred) {
        throw new BadRequestException('Credential not found or does not belong to user');
      }
    }

    return this.prisma.userAiSettings.upsert({
      where: { userId },
      create: { userId, ...dto },
      update: dto,
    });
  }

  // ══ ModelCredentials ══

  async listCredentials(userId: string): Promise<CredentialResponseDto[]> {
    const creds = await this.prisma.userModelCredential.findMany({
      where: { userId, deletedAt: null },
      orderBy: { createdAt: 'desc' },
    });
    return creds.map(c => this.toResponse(c));
  }

  async createCredential(userId: string, dto: CreateCredentialDto): Promise<CredentialResponseDto> {
    const encryptedApiKey = this.crypto.encrypt(dto.apiKey);
    const keyHash = this.crypto.hash(dto.apiKey);
    const maskedKey = this.crypto.mask(dto.apiKey);

    const cred = await this.prisma.userModelCredential.create({
      data: {
        userId,
        provider: dto.provider,
        keyAlias: dto.keyAlias,
        encryptedApiKey,
        keyHash,
        maskedKey,
      },
    });
    return this.toResponse(cred);
  }

  async updateCredential(userId: string, credId: string, dto: UpdateCredentialDto): Promise<CredentialResponseDto> {
    const cred = await this.prisma.userModelCredential.findFirst({
      where: { id: credId, userId, deletedAt: null },
    });
    if (!cred) throw new NotFoundException('Credential not found');

    const encryptedApiKey = this.crypto.encrypt(dto.apiKey);
    const keyHash = this.crypto.hash(dto.apiKey);
    const maskedKey = this.crypto.mask(dto.apiKey);
    const status = 'active';

    const updated = await this.prisma.userModelCredential.update({
      where: { id: credId },
      data: { encryptedApiKey, keyHash, maskedKey, status, keyAlias: dto.keyAlias, lastErrorCode: null, lastErrorMessage: null },
    });
    return this.toResponse(updated);
  }

  async deleteCredential(userId: string, credId: string): Promise<void> {
    const cred = await this.prisma.userModelCredential.findFirst({
      where: { id: credId, userId, deletedAt: null },
    });
    if (!cred) throw new NotFoundException('Credential not found');

    await this.prisma.userModelCredential.update({
      where: { id: credId },
      data: { deletedAt: new Date(), status: 'deleted' },
    });
  }

  async testCredential(userId: string, credId: string) {
    const cred = await this.prisma.userModelCredential.findFirst({
      where: { id: credId, userId, deletedAt: null },
    });
    if (!cred) throw new NotFoundException('Credential not found');

    let decryptedKey: string;
    try {
      decryptedKey = this.crypto.decrypt(cred.encryptedApiKey);
    } catch {
      await this.prisma.userModelCredential.update({
        where: { id: credId },
        data: { status: 'invalid', lastErrorCode: 'DECRYPT_FAILED' },
      });
      return { success: false, errorCode: 'DECRYPT_FAILED', message: '解密 Key 失败，请重新填写' };
    }

    try {
      const baseUrl = this.config.get<string>('ai.deepseek.baseUrl', 'https://api.deepseek.com');
      const res = await fetch(`${baseUrl}/v1/models`, {
        headers: { Authorization: `Bearer ${decryptedKey}` },
        signal: AbortSignal.timeout(10000),
      });

      const success = res.ok;
      await this.prisma.userModelCredential.update({
        where: { id: credId },
        data: {
          status: success ? 'active' : 'invalid',
          lastTestedAt: new Date(),
          lastErrorCode: success ? null : `HTTP_${res.status}`,
          lastErrorMessage: success ? null : `DeepSeek returned ${res.status}`,
        },
      });

      return { success, status: success ? 'active' : 'invalid' };
    } catch (err: any) {
      await this.prisma.userModelCredential.update({
        where: { id: credId },
        data: {
          status: 'invalid',
          lastTestedAt: new Date(),
          lastErrorCode: 'NETWORK_ERROR',
          lastErrorMessage: err.message?.substring(0, 500),
        },
      });
      return { success: false, errorCode: 'NETWORK_ERROR', message: err.message };
    }
  }

  /** 内部使用：解密 key 并返回明文（不写日志） */
  async resolveCredentialForJob(userId: string, credId: string): Promise<{ provider: string; apiKey: string }> {
    const cred = await this.prisma.userModelCredential.findFirst({
      where: { id: credId, userId, deletedAt: null, status: 'active' },
    });
    if (!cred) throw new NotFoundException('Credential not found or not active');

    const apiKey = this.crypto.decrypt(cred.encryptedApiKey);

    await this.prisma.userModelCredential.update({
      where: { id: credId },
      data: { lastUsedAt: new Date() },
    });

    return { provider: cred.provider, apiKey };
  }

  // ══ Analysis Job Creation ══

  async createAnalysisJob(userId: string, dto: CreateAnalysisJobDto): Promise<CreateAnalysisJobResponseDto> {
    // 1. Settings check (auto-create with defaults if new user)
    let settings = await this.prisma.userAiSettings.findUnique({ where: { userId } });
    if (!settings) {
      settings = await this.prisma.userAiSettings.create({ data: { userId } });
    }
    if (!settings.allowAiAnalysis) {
      throw new BadRequestException({ errorCode: 'AI_ANALYSIS_DISABLED', message: 'AI analysis is disabled. Enable it in settings.' });
    }

    // 2. Validate jobType + per-type constraints
    if (!Object.keys(JOB_TYPE_CONFIG).includes(dto.jobType)) {
      throw new BadRequestException({
        errorCode: 'INVALID_JOB_TYPE',
        message: `Invalid jobType "${dto.jobType}". Must be one of: ${Object.keys(JOB_TYPE_CONFIG).join(', ')}`,
      });
    }
    const requiresKnowledgeBase = ['quiz_generation', 'flashcard_generation'];
    if (requiresKnowledgeBase.includes(dto.jobType) && dto.targetType !== 'knowledge_base') {
      throw new BadRequestException({
        errorCode: 'INVALID_TARGET_TYPE',
        message: `${dto.jobType} requires targetType="knowledge_base"`,
      });
    }

    // 3. Idempotency check (cheapest fast-return, before quota/snapshot)
    if (dto.idempotencyKey) {
      const existing = await this.prisma.aiRuntimeJob.findUnique({
        where: { idempotencyKey: dto.idempotencyKey },
        select: { id: true, status: true, createdAt: true },
      });
      if (existing) {
        return { jobId: existing.id, status: existing.status, createdAt: existing.createdAt.toISOString() };
      }
    }

    // 4. Resolve apiKeyMode / credentialId
    // NOTE: apiKeyMode may be reassigned in step 6 fallback — steps 7–11 consume the final values
    let apiKeyMode = dto.apiKeyMode ?? settings.apiKeyMode;
    let credentialId: string | undefined = dto.credentialId ?? settings.defaultCredentialId ?? undefined;
    if (apiKeyMode === 'user_deepseek_key' && !credentialId) {
      throw new BadRequestException({ errorCode: 'CREDENTIAL_REQUIRED', message: 'User key mode requires a credential.' });
    }
    if (credentialId) {
      const cred = await this.prisma.userModelCredential.findFirst({
        where: { id: credentialId, userId, deletedAt: null, status: 'active' },
      });
      if (!cred) {
        throw new NotFoundException({ errorCode: 'CREDENTIAL_NOT_FOUND', message: 'Credential not found or not active.' });
      }
    }

    // 5. Quota check (read-only fast-fail; actual reservation at step 11)
    await this.quota.checkQuota(userId, apiKeyMode);

    // 6. Platform budget check (platform_key only) — with fallback to user key
    if (apiKeyMode === 'platform_key') {
      try {
        await this.budget.checkPlatformBudget('deepseek', 'deepseek-chat');
      } catch (err: any) {
        if (err?.response?.errorCode === 'PLATFORM_CIRCUIT_OPEN' && settings.fallbackToPlatformKey && settings.defaultCredentialId) {
          // Fallback: apiKeyMode + credentialId reassigned — downstream steps use updated values
          apiKeyMode = 'user_deepseek_key';
          credentialId = settings.defaultCredentialId;
        } else {
          throw err;
        }
      }
    }

    // 7. Build snapshot
    const snapshot = await this.snapshotBuilder.buildSnapshot(userId, dto.targetType, dto.targetId);

    // 8. Resolve prompt/output schema versions
    const config = JOB_TYPE_CONFIG[dto.jobType];
    const promptVersion = dto.promptVersion ?? config.promptVersion;
    const outputSchemaVersion = dto.outputSchemaVersion ?? config.outputSchemaVersion;

    // 9. Compute priority
    const profile = await this.prisma.userLearningProfile.findUnique({ where: { userId } });
    const priority = this.priorityRules.computeJobPriority(profile, settings, dto.targetType);

    // 10. Create job + per-type plan records
    const job = await this.prisma.aiRuntimeJob.create({
      data: {
        userId,
        jobType: dto.jobType,
        targetType: dto.targetType,
        targetId: dto.targetId,
        snapshotId: snapshot.id,
        status: 'pending',
        priority,
        idempotencyKey: dto.idempotencyKey ?? null,
        apiKeyMode,
        credentialId: credentialId ?? null,
        modelProvider: 'deepseek',
        modelName: 'deepseek-chat',
        promptVersion,
        outputSchemaVersion,
      },
    });

    // 10. Create per-type plan record
    let planId: string | undefined;
    if (dto.jobType === 'quiz_generation') {
      const plan = await this.prisma.questionGenerationPlan.create({
        data: {
          userId,
          jobId: job.id,
          snapshotId: snapshot.id,
          targetType: dto.targetType,
          targetId: dto.targetId,
          knowledgePointIds: (dto.knowledgePointIds ?? []) as any,
          questionTypes: (dto.questionTypes ?? []) as any,
          difficultyLevel: dto.difficultyLevel ?? null,
          count: dto.questionCount ?? 5,
          status: 'pending',
        },
      });
      planId = plan.id;
    }
    if (dto.jobType === 'flashcard_generation') {
      const plan = await this.prisma.flashcardGenerationPlan.create({
        data: {
          userId,
          jobId: job.id,
          snapshotId: snapshot.id,
          targetType: dto.targetType,
          targetId: dto.targetId,
          knowledgePointIds: (dto.knowledgePointIds ?? []) as any,
          difficultyLevel: dto.difficultyLevel ?? null,
          count: dto.cardCount ?? 5,
          status: 'pending',
        },
      });
      planId = plan.id;
    }

    // 11. Increment quota
    await this.quota.incrementJobCount(userId, apiKeyMode).catch(() => {});

    return { jobId: job.id, status: job.status, createdAt: job.createdAt.toISOString(), ...(planId ? { planId } : {}) };
  }

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

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

    if (job.status === 'locked' || job.status === 'running') {
      await this.prisma.aiRuntimeJob.update({
        where: { id: jobId },
        data: { cancelRequestedAt: new Date() },
      });
      return { jobId, status: 'cancel_requested' };
    }

    throw new BadRequestException({
      errorCode: 'JOB_CANNOT_CANCEL',
      message: `Job is in terminal state "${job.status}"`,
    });
  }

  // ══ Quiz Publish ══

  async publishQuiz(userId: string, quizId: string) {
    const quiz = await this.prisma.quiz.findFirst({
      where: { id: quizId, userId },
      select: { id: true, status: true, knowledgeBaseId: true },
    });
    if (!quiz) throw new NotFoundException({ errorCode: 'QUIZ_NOT_FOUND', message: 'Quiz not found' });
    if (quiz.status !== 'ready') {
      throw new BadRequestException({
        errorCode: 'QUIZ_NOT_READY',
        message: `Quiz is "${quiz.status}", only "ready" quizzes can be published`,
      });
    }

    // Archive old active quizzes for same knowledge base
    if (quiz.knowledgeBaseId) {
      await this.prisma.quiz.updateMany({
        where: { userId, knowledgeBaseId: quiz.knowledgeBaseId, status: 'active', id: { not: quizId } },
        data: { status: 'archived' },
      });
    }

    await this.prisma.quiz.update({
      where: { id: quizId },
      data: { status: 'active' },
    });

    return { quizId, status: 'active' };
  }

  // ══ Flashcard Publish ══

  async publishFlashcard(userId: string, cardId: string) {
    const card = await this.prisma.flashcard.findFirst({
      where: { id: cardId, userId, deletedAt: null },
      select: { id: true, status: true },
    });
    if (!card) throw new NotFoundException({ errorCode: 'FLASHCARD_NOT_FOUND', message: 'Flashcard not found' });
    if (card.status !== 'draft') {
      throw new BadRequestException({
        errorCode: 'FLASHCARD_NOT_DRAFT',
        message: `Flashcard is "${card.status}", only "draft" cards can be published`,
      });
    }

    await this.prisma.flashcard.update({
      where: { id: cardId },
      data: { status: 'active' },
    });

    return { cardId, status: 'active' };
  }

  // ══ Job Status Query ══

  async getJob(userId: string, jobId: string) {
    const job = await this.prisma.aiRuntimeJob.findFirst({
      where: { id: jobId, userId },
      select: {
        id: true, jobType: true, targetType: true, targetId: true,
        status: true, priority: true, snapshotId: true,
        attemptNo: true, retryCount: true, maxRetryCount: true,
        errorCode: true, errorMessage: true,
        cancelRequestedAt: true, cancelledAt: true,
        startedAt: true, finishedAt: true,
        createdAt: true, updatedAt: true,
      },
    });
    if (!job) throw new NotFoundException({ errorCode: 'JOB_NOT_FOUND', message: 'Job not found' });
    return job;
  }

  async listJobs(userId: string, status?: string, take: number = 20) {
    const where: any = { userId };
    if (status) where.status = status;
    return this.prisma.aiRuntimeJob.findMany({
      where,
      select: {
        id: true, jobType: true, targetType: true, targetId: true,
        status: true, priority: true,
        errorCode: true, cancelRequestedAt: true,
        startedAt: true, finishedAt: true, createdAt: true,
      },
      orderBy: { createdAt: 'desc' },
      take: Math.min(take, 50),
    });
  }

  // ══ Analysis Results Query ══

  async getAnalysis(userId: string, analysisId: string) {
    const a = await this.prisma.aiLearningAnalysis.findFirst({
      where: { id: analysisId, userId },
      select: {
        id: true, userId: true, jobId: true, snapshotId: true,
        targetType: true, targetId: true,
        learningState: true, summary: true, riskLevel: true, confidence: true,
        evidence: true, nextActionIds: true,
        promptVersion: true, schemaVersion: true,
        createdAt: true, updatedAt: true,
      },
    });
    if (!a) throw new NotFoundException({ errorCode: 'ANALYSIS_NOT_FOUND', message: 'Analysis not found' });
    return a;
  }

  async listAnalyses(userId: string, targetType?: string, targetId?: string, take: number = 20) {
    const where: any = { userId };
    if (targetType) where.targetType = targetType;
    if (targetId) where.targetId = targetId;
    return this.prisma.aiLearningAnalysis.findMany({
      where,
      select: {
        id: true, targetType: true, targetId: true,
        learningState: true, riskLevel: true, confidence: true,
        summary: true, createdAt: true,
      },
      orderBy: { createdAt: 'desc' },
      take: Math.min(take, 50),
    });
  }

  // ══ Suggestions / Weak Points Query ══

  async listRecommendations(userId: string, targetType?: string, targetId?: string, status?: string, take: number = 20) {
    const where: any = { userId };
    if (targetType) where.targetType = targetType;
    if (targetId) where.targetId = targetId;
    if (status) where.status = status;
    return this.prisma.nextActionRecommendation.findMany({
      where,
      select: {
        id: true, actionType: true, targetType: true, targetId: true,
        title: true, reason: true, priority: true, estimatedMinutes: true,
        deviceSuitability: true, status: true, createdAt: true,
      },
      orderBy: { priority: 'asc' },
      take: Math.min(take, 50),
    });
  }

  async listWeakPoints(userId: string, targetType?: string, targetId?: string, status?: string, take: number = 20) {
    const where: any = { userId };
    if (targetType) where.targetType = targetType;
    if (targetId) where.targetId = targetId;
    if (status) where.status = status;
    return this.prisma.weakPointCandidate.findMany({
      where,
      select: {
        id: true, knowledgePointId: true, title: true, reason: true,
        confidence: true, evidence: true, status: true,
        targetType: true, targetId: true, createdAt: true,
      },
      orderBy: { confidence: 'desc' },
      take: Math.min(take, 50),
    });
  }

  // ══ Quiz Query ══

  async getQuiz(userId: string, quizId: string) {
    const quiz = await this.prisma.quiz.findFirst({
      where: { id: quizId, userId },
      select: {
        id: true, knowledgeBaseId: true, title: true, description: true,
        questionCount: true, sourceType: true, sourceId: true,
        status: true, createdAt: true, updatedAt: true,
      },
    });
    if (!quiz) throw new NotFoundException({ errorCode: 'QUIZ_NOT_FOUND', message: 'Quiz not found' });
    return quiz;
  }

  async getQuizQuestions(userId: string, quizId: string, attemptId?: string) {
    const quiz = await this.prisma.quiz.findFirst({ where: { id: quizId, userId }, select: { id: true } });
    if (!quiz) throw new NotFoundException({ errorCode: 'QUIZ_NOT_FOUND', message: 'Quiz not found' });

    // M-QUIZ-01-01: 若提供了 attemptId，按当前 Attempt 状态控制答案可见性
    // 未提供 attemptId 时，不返回 answer/explanation（安全默认）
    let isFinished = false;
    if (attemptId) {
      const attempt = await this.prisma.quizAttempt.findFirst({
        where: { id: attemptId, userId, quizId },
        select: { finishedAt: true },
      });
      if (attempt) {
        isFinished = attempt.finishedAt !== null;
      }
    }

    return this.prisma.quizQuestion.findMany({
      where: { quizId },
      select: {
        id: true, type: true, stem: true, options: true,
        // M-QUIZ-01-01: 仅当前 Attempt 已提交后才返回 answer 和 explanation
        // 不存在基于"任意历史 Attempt"的答案泄漏
        ...(isFinished ? { answer: true, explanation: true } : {}),
        sourceBlockIds: true, orderIndex: true,
      },
      orderBy: { orderIndex: 'asc' },
    });
  }

  async listQuizzes(userId: string, knowledgeBaseId?: string, status?: string, take: number = 20) {
    const where: any = { userId };
    if (knowledgeBaseId) where.knowledgeBaseId = knowledgeBaseId;
    if (status) where.status = status;
    return this.prisma.quiz.findMany({
      where,
      select: {
        id: true, knowledgeBaseId: true, title: true, questionCount: true,
        sourceType: true, status: true, createdAt: true,
      },
      orderBy: { createdAt: 'desc' },
      take: Math.min(take, 50),
    });
  }

  // ══ Flashcard Query ══

  async getFlashcard(userId: string, cardId: string) {
    const card = await this.prisma.flashcard.findFirst({
      where: { id: cardId, userId, deletedAt: null },
      select: {
        id: true, front: true, back: true, hint: true,
        difficultyLevel: true, knowledgePointId: true, sourceBlockIds: true,
        sourceType: true, sourceId: true, generatedByJobId: true,
        status: true, createdAt: true, updatedAt: true,
      },
    });
    if (!card) throw new NotFoundException({ errorCode: 'FLASHCARD_NOT_FOUND', message: 'Flashcard not found' });
    return card;
  }

  async listFlashcards(userId: string, knowledgePointId?: string, status?: string, take: number = 20) {
    const where: any = { userId, deletedAt: null };
    if (knowledgePointId) where.knowledgePointId = knowledgePointId;
    if (status) where.status = status;
    return this.prisma.flashcard.findMany({
      where,
      select: {
        id: true, front: true, difficultyLevel: true,
        knowledgePointId: true, sourceType: true, status: true, createdAt: true,
      },
      orderBy: { createdAt: 'desc' },
      take: Math.min(take, 50),
    });
  }

  // ══ Re-analysis Trigger ══

  async triggerReanalysis(userId: string, targetType: string, targetId: string) {
    return this.createAnalysisJob(userId, {
      jobType: 'learning_state_analysis',
      targetType,
      targetId,
    });
  }

  // ══ Artifact Feedback ══

  async submitArtifactFeedback(
    userId: string, artifactType: string, artifactId: string,
    dto: { feedbackType: string; reason?: string },
  ) {
    const exists = await this.checkArtifactExists(userId, artifactType, artifactId);
    if (!exists) throw new NotFoundException({ errorCode: 'ARTIFACT_NOT_FOUND', message: `${artifactType} not found` });
    return this.prisma.aiArtifactFeedback.create({
      data: { userId, artifactType, artifactId, feedbackType: dto.feedbackType, reason: dto.reason ?? null },
      select: { id: true, feedbackType: true, createdAt: true },
    });
  }

  private async checkArtifactExists(userId: string, artifactType: string, artifactId: string): Promise<boolean> {
    if (artifactType === 'analysis') {
      const a = await this.prisma.aiLearningAnalysis.findFirst({ where: { id: artifactId, userId }, select: { id: true } });
      return !!a;
    }
    if (artifactType === 'quiz') {
      const q = await this.prisma.quiz.findFirst({ where: { id: artifactId, userId }, select: { id: true } });
      return !!q;
    }
    if (artifactType === 'flashcard') {
      const f = await this.prisma.flashcard.findFirst({ where: { id: artifactId, userId, deletedAt: null }, select: { id: true } });
      return !!f;
    }
    return false;
  }

  // ══ General Feedback ══

  async submitFeedback(userId: string, dto: { category: string; content: string; email?: string; deviceInfo?: any }) {
    return this.prisma.feedback.create({
      data: {
        userId, category: dto.category, content: dto.content,
        email: dto.email ?? null, deviceInfo: dto.deviceInfo ?? undefined, status: 'open',
      },
      select: { id: true, status: true, createdAt: true },
    });
  }

  // ══ Notifications ══

  async listNotifications(userId: string, take: number = 20) {
    return this.prisma.notification.findMany({
      where: { userId },
      select: { id: true, type: true, title: true, content: true, data: true, readAt: true, createdAt: true },
      orderBy: { createdAt: 'desc' },
      take: Math.min(take, 50),
    });
  }

  private toResponse(c: any): CredentialResponseDto {
    return {
      id: c.id,
      provider: c.provider,
      keyAlias: c.keyAlias,
      maskedKey: c.maskedKey,
      status: c.status,
      lastTestedAt: c.lastTestedAt?.toISOString(),
      lastUsedAt: c.lastUsedAt?.toISOString(),
      createdAt: c.createdAt.toISOString(),
      updatedAt: c.updatedAt.toISOString(),
    };
  }
}
