import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsWhere, MoreThanOrEqual, Repository } from 'typeorm';
import { IncidentCaseEntity } from './incident-case.entity';
import { MonitorAlertEntity } from '../monitoring/monitor-alert.entity';
import { MonitorLogEventEntity } from '../monitoring/monitor-log-event.entity';
import { MonitorRunEntity } from '../monitoring/monitor-run.entity';
import { MonitorTargetEntity } from '../monitoring/monitor-target.entity';
import { LogAnalysisReport, LogSeverity } from '../monitoring/log-analysis.types';

const INCIDENT_MERGE_WINDOW_MS = 30 * 60 * 1000;
const INCIDENT_REBUILD_SPLIT_WINDOW_MS = 45 * 60 * 1000;
const ALERT_STORM_THRESHOLD = 3;
const ALERT_STORM_OCCURRENCE_THRESHOLD = 8;

type CategoryContext = {
  rootCategory: string;
  categoryKey: string;
  dominantCategory: string;
  uniqueCategories: string[];
};

type StormContext = {
  stormMode: boolean;
  uniqueCategoryCount: number;
  totalAlertCount: number;
  totalOccurrenceCount: number;
  collapsedAlertIds: string[];
};

type IncidentBucket = {
  serviceKey: string;
  rootCategory: string;
  categoryKey: string;
  dominantCategory: string;
  alerts: MonitorAlertEntity[];
  startedAt: Date;
  endedAt: Date;
};

@Injectable()
export class IncidentsService {
  constructor(
    @InjectRepository(IncidentCaseEntity)
    private readonly incidentRepository: Repository<IncidentCaseEntity>,
    @InjectRepository(MonitorAlertEntity)
    private readonly alertRepository: Repository<MonitorAlertEntity>,
    @InjectRepository(MonitorRunEntity)
    private readonly runRepository: Repository<MonitorRunEntity>,
    @InjectRepository(MonitorLogEventEntity)
    private readonly logEventRepository: Repository<MonitorLogEventEntity>,
    @InjectRepository(MonitorTargetEntity)
    private readonly targetRepository: Repository<MonitorTargetEntity>,
  ) {}

  async syncCaseForRun(
    run: MonitorRunEntity & { target: MonitorTargetEntity },
    analysis: LogAnalysisReport,
    alerts: MonitorAlertEntity[],
  ) {
    if (!alerts.length) {
      await this.resolveCasesForTarget(
        run.target.serviceKey,
        run.target.type,
        '相关监测与日志分析恢复正常，自动关闭同目标类型的聚合研判对象。',
      );
      return null;
    }

    const now = new Date();
    const category = this.deriveCategoryContext(
      run.target.type,
      alerts.map((item) => item.category),
      analysis.issues.map((item) => item.category),
      run.errorTag,
    );
    const storm = this.buildStormContext(alerts);
    const existing = await this.findMergeCandidate(
      run.target.serviceKey,
      category.categoryKey,
      now,
    );

    const title =
      alerts[0]?.title ??
      analysis.issues[0]?.title ??
      `${run.target.name} 综合研判事件`;
    const severity = this.pickSeverity(run, analysis, alerts);
    const recommendationActions = this.collectRecommendedActions(alerts, analysis);
    const evidencePoints = this.collectEvidencePoints(alerts, analysis);
    const summary = this.buildSummary(run, analysis, alerts, storm);

    const incident =
      existing ??
      this.incidentRepository.create({
        serviceKey: run.target.serviceKey,
        title,
        severity,
        status: 'open',
        summary,
        categoryKey: category.categoryKey,
        rootCategory: category.rootCategory,
        latestTargetId: run.targetId,
        latestRunId: run.id,
        latestAlertIdsJson: null,
        sourceSummaryJson: null,
        evidenceJson: null,
        diagnosisJson: null,
        recommendationJson: null,
        firstDetectedAt: now,
        lastDetectedAt: now,
        occurrenceCount: 0,
        stormScore: 0,
      });

    incident.title = title;
    incident.severity = severity;
    incident.summary = summary;
    incident.categoryKey = category.categoryKey;
    incident.rootCategory = category.rootCategory;
    incident.latestTargetId = run.targetId;
    incident.latestRunId = run.id;
    incident.lastDetectedAt = now;
    incident.occurrenceCount = (incident.occurrenceCount ?? 0) + 1;
    incident.stormScore = storm.totalOccurrenceCount;
    if (incident.status === 'resolved') {
      incident.status = 'open';
    }

    incident.latestAlertIdsJson = JSON.stringify(alerts.map((item) => item.id));
    incident.sourceSummaryJson = JSON.stringify({
      target: {
        id: run.target.id,
        name: run.target.name,
        type: run.target.type,
        endpoint: run.target.endpoint,
      },
      run: {
        id: run.id,
        traceId: run.traceId,
        status: run.status,
        errorTag: run.errorTag,
        summary: run.summary,
        failureReason: run.failureReason,
        startedAt: run.startedAt,
        endedAt: run.endedAt,
        resultJsonUrl: run.resultJsonUrl,
      },
      category,
      storm,
      alerts: alerts.map((item) => ({
        id: item.id,
        category: item.category,
        severity: item.severity,
        status: item.status,
        summary: item.summary,
        occurrenceCount: item.occurrenceCount,
      })),
    });
    incident.evidenceJson = JSON.stringify({
      resultJsonUrl: run.resultJsonUrl,
      logExcerpt: run.logExcerpt,
      logStats: analysis.stats,
      topTemplates: analysis.templates.slice(0, 3).map((item) => ({
        templateId: item.templateId,
        category: item.category,
        severity: item.severity,
        sampleCount: item.sampleCount,
      })),
      evidencePoints,
    });
    incident.diagnosisJson = JSON.stringify({
      overview: analysis.overview,
      severity: analysis.severity,
      source: analysis.source,
      issues: analysis.issues,
      dominantCategory: category.dominantCategory,
    });
    incident.recommendationJson = JSON.stringify({
      actions: recommendationActions,
      nextFocus: analysis.issues.map((item) => item.title).slice(0, 3),
    });

    return this.incidentRepository.save(incident);
  }

  async getIncidents(filters: {
    status?: 'open' | 'acknowledged' | 'resolved';
    severity?: 'info' | 'warning' | 'major' | 'critical';
    serviceKey?: string;
    limit?: number;
  }) {
    const query = this.incidentRepository
      .createQueryBuilder('incident')
      .orderBy('incident.lastDetectedAt', 'DESC');

    if (filters.status) {
      query.andWhere('incident.status = :status', { status: filters.status });
    }
    if (filters.severity) {
      query.andWhere('incident.severity = :severity', {
        severity: filters.severity,
      });
    }
    if (filters.serviceKey) {
      query.andWhere('incident.serviceKey = :serviceKey', {
        serviceKey: filters.serviceKey,
      });
    }

    const take = Math.min(Math.max(filters.limit ?? 100, 1), 300);
    const [list, total] = await query.take(take).getManyAndCount();

    return {
      list: list.map((item) => this.expandIncident(item)),
      total,
    };
  }

  async rebuildFromCurrentData() {
    await this.incidentRepository.clear();

    const alerts = await this.alertRepository.find({
      order: { lastDetectedAt: 'ASC' },
    });
    const targetIds = Array.from(
      new Set(alerts.map((item) => item.targetId).filter(Boolean)),
    );
    const targets = targetIds.length
      ? await this.targetRepository.find({
          where: targetIds.map((id) => ({ id })) as FindOptionsWhere<MonitorTargetEntity>[],
        })
      : [];
    const targetMap = new Map(targets.map((item) => [item.id, item]));

    const grouped = new Map<string, MonitorAlertEntity[]>();
    for (const alert of alerts) {
      const key = alert.serviceKey.trim();
      if (!key) continue;
      const current = grouped.get(key) ?? [];
      current.push(alert);
      grouped.set(key, current);
    }

    let created = 0;
    for (const [serviceKey, serviceAlerts] of grouped.entries()) {
      const serviceRuns = await this.runRepository
        .createQueryBuilder('run')
        .leftJoinAndSelect('run.target', 'target')
        .where('target.serviceKey = :serviceKey', { serviceKey })
        .orderBy('run.startedAt', 'ASC')
        .getMany();

      const buckets = this.buildBucketsForService(
        serviceKey,
        serviceAlerts,
        targetMap,
      );
      for (const bucket of buckets) {
        const latestRun = this.pickNearestRelevantRun(serviceRuns, bucket.endedAt);
        const storm = this.buildStormContext(bucket.alerts);
        const evidencePoints = this.collectRecommendedEvidenceFromAlerts(bucket.alerts);
        const recommendationActions = this.collectRecommendedActionsFromAlerts(
          bucket.alerts,
        );
        const status = this.pickIncidentStatusFromAlerts(bucket.alerts);
        const summary = this.buildRebuiltSummary(bucket, latestRun, storm);

        await this.incidentRepository.save(
          this.incidentRepository.create({
            serviceKey,
            title: bucket.alerts[0]?.title ?? `${serviceKey} 综合研判事件`,
            severity: this.pickSeverityFromAlerts(bucket.alerts),
            status,
            summary,
            categoryKey: bucket.categoryKey,
            rootCategory: bucket.rootCategory,
            latestTargetId:
              bucket.alerts[bucket.alerts.length - 1]?.targetId ??
              latestRun?.targetId ??
              null,
            latestRunId:
              latestRun?.id ??
              bucket.alerts[bucket.alerts.length - 1]?.runId ??
              null,
            latestAlertIdsJson: JSON.stringify(bucket.alerts.map((item) => item.id)),
            sourceSummaryJson: JSON.stringify({
              rebuild: true,
              category: {
                rootCategory: bucket.rootCategory,
                categoryKey: bucket.categoryKey,
                dominantCategory: bucket.dominantCategory,
              },
              storm,
              latestRun: latestRun
                ? {
                    id: latestRun.id,
                    traceId: latestRun.traceId,
                    status: latestRun.status,
                    errorTag: latestRun.errorTag,
                    summary: latestRun.summary,
                    failureReason: latestRun.failureReason,
                    resultJsonUrl: latestRun.resultJsonUrl,
                  }
                : null,
              alerts: bucket.alerts.map((item) => ({
                id: item.id,
                category: item.category,
                severity: item.severity,
                summary: item.summary,
                status: item.status,
                occurrenceCount: item.occurrenceCount,
              })),
            }),
            evidenceJson: JSON.stringify({
              resultJsonUrl: latestRun?.resultJsonUrl ?? null,
              logExcerpt: latestRun?.logExcerpt ?? null,
              evidencePoints,
            }),
            diagnosisJson: JSON.stringify({
              rebuiltFrom: 'alerts-and-runs',
              dominantCategory: bucket.dominantCategory,
              alertCount: bucket.alerts.length,
              categories: Array.from(
                new Set(bucket.alerts.map((item) => item.category)),
              ),
            }),
            recommendationJson: JSON.stringify({
              actions: recommendationActions,
            }),
            firstDetectedAt: bucket.startedAt,
            lastDetectedAt: bucket.endedAt,
            occurrenceCount: bucket.alerts.reduce(
              (total, item) => total + (item.occurrenceCount ?? 1),
              0,
            ),
            stormScore: storm.totalOccurrenceCount,
          }),
        );
        created += 1;
      }
    }

    return {
      rebuilt: true,
      incidentCount: created,
      sourceAlertCount: alerts.length,
    };
  }

  async getIncidentDetail(id: string) {
    const incident = await this.incidentRepository.findOneBy({ id });
    if (!incident) {
      throw new NotFoundException('综合研判对象不存在');
    }

    const windowStart = new Date(
      incident.firstDetectedAt.getTime() - INCIDENT_MERGE_WINDOW_MS,
    );
    const windowEnd = new Date(
      incident.lastDetectedAt.getTime() + INCIDENT_MERGE_WINDOW_MS,
    );
    const targetType = this.readTargetTypeFromCategoryKey(incident.categoryKey);

    const [targets, alerts, recentRuns, recentLogs] = await Promise.all([
      this.targetRepository.find({
        where: { serviceKey: incident.serviceKey },
        order: { updatedAt: 'DESC' },
        take: 10,
      }),
      this.alertRepository.find({
        where: {
          serviceKey: incident.serviceKey,
          lastDetectedAt: MoreThanOrEqual(windowStart),
        },
        order: { lastDetectedAt: 'DESC' },
        take: 20,
      }),
      this.runRepository
        .createQueryBuilder('run')
        .leftJoinAndSelect('run.target', 'target')
        .where('target.serviceKey = :serviceKey', {
          serviceKey: incident.serviceKey,
        })
        .andWhere('run.startedAt <= :windowEnd', { windowEnd })
        .andWhere('run.startedAt >= :windowStart', { windowStart })
        .orderBy('run.startedAt', 'DESC')
        .take(10)
        .getMany(),
      this.logEventRepository.find({
        where: {
          serviceKey: incident.serviceKey,
          eventAt: MoreThanOrEqual(windowStart),
        },
        order: { eventAt: 'DESC' },
        take: 20,
      }),
    ]);

    const filteredAlerts = alerts.filter((item) => {
      const alertTime = item.lastDetectedAt.getTime();
      if (alertTime > windowEnd.getTime()) return false;
      return this.buildCategoryKey(
        this.normalizeCategory(item.category),
        targetType ?? undefined,
      ) === incident.categoryKey;
    });

    return {
      incident: this.expandIncident(incident),
      detail: {
        targets: targetType
          ? targets.filter((item) => item.type === targetType)
          : targets,
        alerts: filteredAlerts,
        recentRuns: targetType
          ? recentRuns.filter((item) => item.target?.type === targetType)
          : recentRuns,
        recentLogs,
      },
    };
  }

  async updateIncidentStatus(
    id: string,
    status: 'open' | 'acknowledged' | 'resolved',
  ) {
    const incident = await this.incidentRepository.findOneBy({ id });
    if (!incident) {
      throw new NotFoundException('综合研判对象不存在');
    }
    incident.status = status;
    return this.incidentRepository.save(incident);
  }

  async resolveCasesForTarget(
    serviceKey: string,
    targetType: string,
    message: string,
  ) {
    const list = await this.incidentRepository.find({
      where: [
        { serviceKey, status: 'open' },
        { serviceKey, status: 'acknowledged' },
      ],
    });

    const next = list.filter((item) =>
      item.categoryKey.endsWith(`:${targetType}`),
    );
    for (const item of next) {
      item.status = 'resolved';
      item.summary = message;
      item.lastDetectedAt = new Date();
    }

    if (next.length) {
      await this.incidentRepository.save(next);
    }
  }

  private async findMergeCandidate(
    serviceKey: string,
    categoryKey: string,
    now: Date,
  ) {
    const cutoff = new Date(now.getTime() - INCIDENT_MERGE_WINDOW_MS);
    return this.incidentRepository.findOne({
      where: [
        {
          serviceKey,
          categoryKey,
          status: 'open',
          lastDetectedAt: MoreThanOrEqual(cutoff),
        },
        {
          serviceKey,
          categoryKey,
          status: 'acknowledged',
          lastDetectedAt: MoreThanOrEqual(cutoff),
        },
      ],
      order: { lastDetectedAt: 'DESC' },
    });
  }

  private buildBucketsForService(
    serviceKey: string,
    alerts: MonitorAlertEntity[],
    targetMap: Map<string, MonitorTargetEntity>,
  ) {
    const buckets: IncidentBucket[] = [];

    for (const alert of alerts.sort(
      (left, right) =>
        left.lastDetectedAt.getTime() - right.lastDetectedAt.getTime(),
    )) {
      const targetType = targetMap.get(alert.targetId)?.type ?? 'mixed';
      const category = this.deriveCategoryContext(
        targetType,
        [alert.category],
        [],
        null,
      );
      const current = buckets[buckets.length - 1];
      const canMerge =
        current &&
        current.serviceKey === serviceKey &&
        current.categoryKey === category.categoryKey &&
        alert.lastDetectedAt.getTime() - current.endedAt.getTime() <=
          INCIDENT_REBUILD_SPLIT_WINDOW_MS;

      if (!canMerge) {
        buckets.push({
          serviceKey,
          rootCategory: category.rootCategory,
          categoryKey: category.categoryKey,
          dominantCategory: category.dominantCategory,
          alerts: [alert],
          startedAt: alert.firstDetectedAt,
          endedAt: alert.lastDetectedAt,
        });
        continue;
      }

      current.alerts.push(alert);
      current.endedAt = alert.lastDetectedAt;
    }

    return buckets;
  }

  private pickNearestRelevantRun(
    runs: Array<MonitorRunEntity & { target?: MonitorTargetEntity }>,
    endedAt: Date,
  ) {
    const meaningful = runs
      .filter((item) => item.status !== 'queued' && item.status !== 'running')
      .filter((item) => item.startedAt.getTime() <= endedAt.getTime())
      .sort((left, right) => right.startedAt.getTime() - left.startedAt.getTime());

    if (meaningful.length) {
      return meaningful[0];
    }

    return runs
      .filter((item) => item.startedAt.getTime() <= endedAt.getTime())
      .sort((left, right) => right.startedAt.getTime() - left.startedAt.getTime())[0] ?? null;
  }

  private deriveCategoryContext(
    targetType: string,
    alertCategories: string[],
    issueCategories: string[],
    runErrorTag?: string | null,
  ): CategoryContext {
    const candidates = [
      ...alertCategories,
      ...issueCategories,
      ...(runErrorTag ? [runErrorTag] : []),
    ]
      .map((item) => item.trim())
      .filter(Boolean);

    const normalized = candidates.map((item) => this.normalizeCategory(item));
    const dominant = this.pickDominantCategory(normalized);
    const uniqueCategories = Array.from(new Set(candidates));

    return {
      rootCategory: dominant,
      categoryKey: this.buildCategoryKey(dominant, targetType),
      dominantCategory: candidates[0] ?? dominant,
      uniqueCategories,
    };
  }

  private buildStormContext(alerts: MonitorAlertEntity[]): StormContext {
    const totalOccurrenceCount = alerts.reduce(
      (total, item) => total + (item.occurrenceCount ?? 1),
      0,
    );
    const uniqueCategoryCount = new Set(alerts.map((item) => item.category)).size;

    return {
      stormMode:
        alerts.length >= ALERT_STORM_THRESHOLD ||
        totalOccurrenceCount >= ALERT_STORM_OCCURRENCE_THRESHOLD,
      uniqueCategoryCount,
      totalAlertCount: alerts.length,
      totalOccurrenceCount,
      collapsedAlertIds: alerts.map((item) => item.id),
    };
  }

  private buildSummary(
    run: MonitorRunEntity,
    analysis: LogAnalysisReport,
    alerts: MonitorAlertEntity[],
    storm: StormContext,
  ) {
    const base =
      analysis.overview ||
      run.summary ||
      run.failureReason ||
      alerts[0]?.summary ||
      '当前检测到需要进一步跟进的综合异常。';

    if (!storm.stormMode) {
      return base;
    }

    return `${base} 近窗口内已压缩 ${storm.totalAlertCount} 条告警、${storm.totalOccurrenceCount} 次命中，避免告警风暴放大。`;
  }

  private buildRebuiltSummary(
    bucket: IncidentBucket,
    latestRun: (MonitorRunEntity & { target?: MonitorTargetEntity }) | null,
    storm: StormContext,
  ) {
    const base =
      bucket.alerts[0]?.summary ??
      latestRun?.summary ??
      latestRun?.failureReason ??
      '根据存量数据回灌生成的综合研判对象。';

    if (!storm.stormMode) {
      return base;
    }

    return `${base} 回灌时已聚合 ${storm.totalAlertCount} 条历史告警、${storm.totalOccurrenceCount} 次命中。`;
  }

  private collectEvidencePoints(
    alerts: MonitorAlertEntity[],
    analysis: LogAnalysisReport,
  ) {
    const fromAlerts = alerts.flatMap((alert) => {
      const detail = this.parseJson(alert.detailsJson) as { evidence?: string[] };
      return Array.isArray(detail.evidence) ? detail.evidence : [];
    });

    return Array.from(
      new Set(
        [...fromAlerts, ...analysis.issues.flatMap((item) => item.evidence)]
          .map((item) => item.trim())
          .filter(Boolean),
      ),
    ).slice(0, 12);
  }

  private collectRecommendedActions(
    alerts: MonitorAlertEntity[],
    analysis: LogAnalysisReport,
  ) {
    const fromAlerts = alerts.flatMap((alert) => {
      const detail = this.parseJson(alert.detailsJson) as {
        recommendedActions?: string[];
      };
      return Array.isArray(detail.recommendedActions)
        ? detail.recommendedActions
        : [];
    });

    return Array.from(
      new Set(
        [...fromAlerts, ...analysis.issues.flatMap((item) => item.recommendedActions)]
          .map((item) => item.trim())
          .filter(Boolean),
      ),
    ).slice(0, 8);
  }

  private collectRecommendedEvidenceFromAlerts(alerts: MonitorAlertEntity[]) {
    return Array.from(
      new Set(
        alerts.flatMap((alert) => {
          const detail = this.parseJson(alert.detailsJson) as {
            evidence?: string[];
          };
          return Array.isArray(detail.evidence) ? detail.evidence : [];
        }),
      ),
    ).slice(0, 12);
  }

  private collectRecommendedActionsFromAlerts(alerts: MonitorAlertEntity[]) {
    return Array.from(
      new Set(
        alerts.flatMap((alert) => {
          const detail = this.parseJson(alert.detailsJson) as {
            recommendedActions?: string[];
          };
          return Array.isArray(detail.recommendedActions)
            ? detail.recommendedActions
            : [];
        }),
      ),
    ).slice(0, 8);
  }

  private pickSeverity(
    run: MonitorRunEntity,
    analysis: LogAnalysisReport,
    alerts: MonitorAlertEntity[],
  ): LogSeverity {
    const severities = [
      analysis.severity,
      ...alerts.map((item) => item.severity),
      ...(run.status === 'failed'
        ? (['major'] as LogSeverity[])
        : run.status === 'partial'
          ? (['warning'] as LogSeverity[])
          : []),
    ];

    if (severities.includes('critical')) return 'critical';
    if (severities.includes('major')) return 'major';
    if (severities.includes('warning')) return 'warning';
    return 'info';
  }

  private pickSeverityFromAlerts(alerts: MonitorAlertEntity[]): LogSeverity {
    const severities = alerts.map((item) => item.severity);
    if (severities.includes('critical')) return 'critical';
    if (severities.includes('major')) return 'major';
    if (severities.includes('warning')) return 'warning';
    return 'info';
  }

  private pickIncidentStatusFromAlerts(alerts: MonitorAlertEntity[]) {
    if (alerts.some((item) => item.status === 'open')) return 'open';
    if (alerts.some((item) => item.status === 'acknowledged')) {
      return 'acknowledged';
    }
    return 'resolved';
  }

  private pickDominantCategory(categories: string[]) {
    const counts = new Map<string, number>();
    for (const item of categories) {
      counts.set(item, (counts.get(item) ?? 0) + 1);
    }

    return Array.from(counts.entries()).sort((left, right) => {
      if (right[1] !== left[1]) {
        return right[1] - left[1];
      }
      return this.rankCategory(right[0]) - this.rankCategory(left[0]);
    })[0]?.[0] ?? 'availability';
  }

  private buildCategoryKey(rootCategory: string, targetType?: string) {
    return `${rootCategory}:${targetType ?? 'mixed'}`;
  }

  private readTargetTypeFromCategoryKey(categoryKey: string) {
    const [, targetType] = categoryKey.split(':');
    return targetType && targetType !== 'mixed' ? targetType : null;
  }

  private normalizeCategory(value: string) {
    const source = value.toLowerCase();
    if (/database|jdbc|mysql|redis_ping|redis|connectivity|sql|communications/i.test(source)) return 'database';
    if (/application-runtime|runtime|module|exception|java_health_failed|oom|deadlock/i.test(source)) return 'runtime';
    if (/timeout|tcp|network|probe|http_check_failed|blackbox|connect_failed/i.test(source)) return 'network';
    if (/scheduler|queued|stale|backlog/i.test(source)) return 'scheduler';
    if (/task-config|config|auth|permission|token|denied/i.test(source)) return 'config';
    if (/agent|heartbeat|ingest/i.test(source)) return 'agent';
    if (/port-conflict|eaddrinuse|address already in use/i.test(source)) return 'runtime';
    if (/unknown|execution_error|run-status/i.test(source)) return 'availability';
    return 'availability';
  }

  private rankCategory(value: string) {
    return {
      database: 6,
      runtime: 5,
      network: 4,
      scheduler: 3,
      config: 2,
      agent: 1,
      availability: 0,
    }[value] ?? 0;
  }

  private expandIncident(item: IncidentCaseEntity) {
    return {
      ...item,
      latestAlertIds: this.parseJsonArray<string>(item.latestAlertIdsJson),
      sourceSummary: this.parseJson(item.sourceSummaryJson),
      evidence: this.parseJson(item.evidenceJson),
      diagnosis: this.parseJson(item.diagnosisJson),
      recommendation: this.parseJson(item.recommendationJson),
    };
  }

  private parseJson(raw: string | null) {
    try {
      return raw ? JSON.parse(raw) : {};
    } catch {
      return {};
    }
  }

  private parseJsonArray<T>(raw: string | null) {
    try {
      const parsed = raw ? JSON.parse(raw) : [];
      return Array.isArray(parsed) ? (parsed as T[]) : [];
    } catch {
      return [];
    }
  }
}
