import { Inject, Injectable, NotFoundException, Optional } from '@nestjs/common';
import { JobsService } from '../jobs/jobs.service';
import { XiaobaoAiService } from '../xiaobao-ai/xiaobao-ai.service';
import {
  RefreshXiaobaoRiskOptions,
  XIAOBAO_PRISMA,
  XIAOBAO_SUMMARY_REFRESH_JOB,
  XiaobaoRiskLevel,
  XiaobaoRiskSummaryPayload,
} from './xiaobao-risk.types';

const WORK_HOURS_PER_DAY = 8;
const DEV_PROGRESS: Record<string, number> = {
  todo: 0,
  in_progress: 40,
  testing: 80,
  submitted: 100,
};
const OPEN_BUG_STATUSES = new Set(['open', 'fixing', 'fixed', 'verifying']);

@Injectable()
export class XiaobaoRiskService {
  constructor(
    @Inject(XIAOBAO_PRISMA) private readonly prisma: any,
    private readonly jobs: JobsService,
    @Optional() private readonly xiaobaoAi?: XiaobaoAiService,
  ) {}

  async markDirtyAndEnqueue(versionId: string) {
    const riskSignature = `dirty:${versionId}`;
    await this.prisma.xiaobaoRiskSummary.upsert({
      where: { versionId },
      update: { dirty: true },
      create: {
        versionId,
        riskLevel: 'attention',
        riskScore: 1,
        confidence: 0,
        riskSignature,
        summary: {
          versionId,
          riskLevel: 'attention',
          riskScore: 1,
          confidence: 0,
          riskSignature,
          dirty: true,
        },
        dirty: true,
      },
    });

    return this.jobs.enqueue({
      type: XIAOBAO_SUMMARY_REFRESH_JOB,
      dedupeKey: versionId,
      payload: { versionId },
      maxAttempts: 5,
    });
  }

  countDirtySummaries(): Promise<number> {
    return this.prisma.xiaobaoRiskSummary.count({ where: { dirty: true } });
  }

  async refreshSummary(versionId: string, options: RefreshXiaobaoRiskOptions = {}): Promise<XiaobaoRiskSummaryPayload> {
    const now = options.now ?? new Date();
    const version = await this.prisma.version.findUnique({
      where: { id: versionId },
      include: {
        product: { select: { id: true, name: true } },
        project: { select: { id: true, name: true } },
      },
    });
    if (!version) throw new NotFoundException('Version not found');

    const [devTasks, testCases, bugs, activities, worklogs] = await Promise.all([
      this.prisma.devTask.findMany({ where: { versionId } }),
      this.prisma.testCase.findMany({ where: { versionId } }),
      this.prisma.bug.findMany({ where: { versionId } }),
      this.prisma.workActivity.findMany({
        where: { versionId },
        orderBy: { occurredAt: 'desc' },
        take: 200,
      }),
      this.prisma.taskWorklog.findMany({
        where: { versionId },
        orderBy: { workDate: 'desc' },
        take: 200,
      }),
    ]);

    const payload = calculateRiskPayload({
      version,
      devTasks,
      testCases,
      bugs,
      activities,
      worklogs,
      now,
    });
    const forecastReleaseDate = payload.forecastReleaseDate ? new Date(payload.forecastReleaseDate) : null;
    const data = {
      riskLevel: payload.riskLevel,
      riskScore: payload.riskScore,
      confidence: payload.confidence,
      forecastReleaseDate,
      riskSignature: payload.riskSignature,
      summary: payload,
      dirty: false,
      recomputedAt: now,
    };

    await this.prisma.xiaobaoRiskSummary.upsert({
      where: { versionId },
      update: data,
      create: {
        versionId,
        ...data,
      },
    });

    await this.xiaobaoAi?.evaluateSummary(payload, { now });

    return payload;
  }
}

function calculateRiskPayload(input: {
  version: any;
  devTasks: any[];
  testCases: any[];
  bugs: any[];
  activities: any[];
  worklogs: any[];
  now: Date;
}): XiaobaoRiskSummaryPayload {
  const { version, devTasks, testCases, bugs, activities, worklogs, now } = input;
  const remainingDevHours = devTasks.reduce((sum, task) => {
    const estimate = getEstimateHours(task, 8);
    const progress = DEV_PROGRESS[String(task.status)] ?? 0;
    return sum + estimate * Math.max(0, 100 - progress) / 100;
  }, 0);
  const remainingTestHours = testCases.reduce((sum, testCase) => {
    if (testCase.status === 'passed') return sum;
    return sum + getEstimateHours(testCase, 4);
  }, 0);
  const openBugs = bugs.filter((bug) => OPEN_BUG_STATUSES.has(String(bug.status)));
  const remainingBugHours = openBugs.reduce((sum, bug) => sum + getBugEstimateHours(bug), 0);
  const remainingWorkHours = roundHours(remainingDevHours + remainingTestHours + remainingBugHours);

  const forecastReleaseDate = remainingWorkHours > 0 ? addWorkHours(now, remainingWorkHours).toISOString() : undefined;
  const expectedReleaseDate = toIsoOrNull(version.expectedReleaseDate);
  const expectedRelease = expectedReleaseDate ? new Date(expectedReleaseDate) : undefined;
  const forecast = forecastReleaseDate ? new Date(forecastReleaseDate) : undefined;
  const delayDays = expectedRelease && forecast && forecast.getTime() > expectedRelease.getTime()
    ? roundDays((forecast.getTime() - expectedRelease.getTime()) / 86_400_000)
    : 0;
  const daysToExpectedRelease = expectedRelease
    ? roundDays((expectedRelease.getTime() - now.getTime()) / 86_400_000)
    : undefined;

  const criticalBugCount = openBugs.filter((bug) => bug.severity === 'critical' || Number(bug.priority) <= 1).length;
  const failedTestCount = testCases.filter((testCase) => testCase.status === 'failed').length;
  const blockedCount = devTasks.filter((task) => Boolean(task.isBlocked)).length
    + testCases.filter((testCase) => testCase.status === 'blocked').length;
  const unfinishedCount = devTasks.filter((task) => task.status !== 'submitted').length
    + testCases.filter((testCase) => testCase.status !== 'passed').length
    + openBugs.length;
  const recentActivityCount = countRecentActivity(activities, worklogs, now);
  const lastActivityAt = latestIso([
    ...activities.map((item) => toIsoOrUndefined(item.occurredAt)),
    ...worklogs.map((item) => toIsoOrUndefined(item.createdAt)),
  ]);

  const reasons = buildReasons({
    remainingWorkHours,
    delayDays,
    criticalBugCount,
    failedTestCount,
    blockedCount,
  });
  const riskScore = calcRiskScore({
    delayDays,
    remainingWorkHours,
    criticalBugCount,
    failedTestCount,
    blockedCount,
    silentRiskCount: 0,
    daysToExpectedRelease,
  });
  const riskLevel = getRiskLevel(riskScore, delayDays, criticalBugCount > 0 || blockedCount > 0);
  const confidence = calcConfidence({
    version,
    devTasks,
    testCases,
    bugs,
    recentActivityCount,
  });
  const signals = {
    unfinishedCount,
    openBugCount: openBugs.length,
    criticalBugCount,
    failedTestCount,
    blockedCount,
    silentRiskCount: 0,
    daysToExpectedRelease,
  };
  const riskSignature = [
    version.id,
    riskLevel,
    riskScore,
    unfinishedCount,
    openBugs.length,
    criticalBugCount,
    failedTestCount,
    blockedCount,
  ].join('|');

  return {
    versionId: version.id,
    versionName: version.name,
    productId: version.productId,
    productName: version.product?.name,
    projectId: version.projectId,
    projectName: version.project?.name,
    expectedReleaseDate,
    riskScore,
    riskLevel,
    confidence,
    forecastReleaseDate,
    delayDays,
    remainingWorkHours,
    riskSignature,
    signals,
    reasons,
    dailyEvidence: {
      todayDeliveries: [],
      todayProgress: [],
      todayCreations: [],
      todayRisks: [],
      progressNotes: [],
      needsProgressItems: [],
      recentActivityCount,
      totalActivityCount: activities.length,
      todayActualHours: calcTodayWorklogHours(worklogs, now),
      lastActivityAt,
      silentRisks: [],
    },
    currentSnapshot: {
      versionId: version.id,
      date: now.toISOString().slice(0, 10),
      riskScore,
      riskLevel,
      forecastReleaseDate,
      openBugCount: openBugs.length,
      criticalBugCount,
      failedTestCount,
      blockedCount,
      silentRiskCount: 0,
      confidence,
      createdAt: now.toISOString(),
    },
  };
}

function getEstimateHours(item: any, fallback: number): number {
  if (typeof item.estimateHours === 'number' && item.estimateHours > 0) return roundHours(item.estimateHours);
  if (typeof item.aiEstimateHours === 'number' && item.aiEstimateHours > 0) return roundHours(item.aiEstimateHours);
  return fallback;
}

function getBugEstimateHours(bug: any): number {
  const estimate = getEstimateHours(bug, 0);
  if (estimate > 0) return estimate;
  if (bug.severity === 'critical') return 16;
  if (bug.severity === 'major') return 8;
  return 4;
}

function buildReasons(input: {
  remainingWorkHours: number;
  delayDays: number;
  criticalBugCount: number;
  failedTestCount: number;
  blockedCount: number;
}): XiaobaoRiskSummaryPayload['reasons'] {
  const reasons: XiaobaoRiskSummaryPayload['reasons'] = [];
  if (input.remainingWorkHours > 0) {
    reasons.push({
      key: 'remaining_work',
      title: '剩余工作量',
      detail: `预计还剩 ${input.remainingWorkHours}h 工作量。`,
      severity: input.delayDays > 0 ? 'danger' : 'warning',
    });
  }
  if (input.delayDays > 0) {
    reasons.push({
      key: 'forecast_delay',
      title: '预测延期',
      detail: `预测发布时间晚于计划约 ${input.delayDays} 天。`,
      severity: 'danger',
    });
  }
  if (input.criticalBugCount > 0) {
    reasons.push({
      key: 'critical_bug',
      title: '关键缺陷',
      detail: `仍有 ${input.criticalBugCount} 个 P1 或致命 Bug 未关闭。`,
      severity: 'danger',
      count: input.criticalBugCount,
    });
  }
  if (input.blockedCount > 0) {
    reasons.push({
      key: 'blocked_work',
      title: '阻塞工作',
      detail: `仍有 ${input.blockedCount} 个开发或测试项处于阻塞。`,
      severity: 'danger',
      count: input.blockedCount,
    });
  }
  if (input.failedTestCount > 0) {
    reasons.push({
      key: 'failed_test',
      title: '失败用例',
      detail: `仍有 ${input.failedTestCount} 个测试用例未通过。`,
      severity: 'warning',
      count: input.failedTestCount,
    });
  }
  return reasons;
}

function calcRiskScore(input: {
  delayDays: number;
  remainingWorkHours: number;
  criticalBugCount: number;
  failedTestCount: number;
  blockedCount: number;
  silentRiskCount: number;
  daysToExpectedRelease?: number;
}): number {
  let score = 0;
  if (input.delayDays > 0) score += 70 + Math.min(15, input.delayDays * 3);
  if (input.daysToExpectedRelease !== undefined && input.daysToExpectedRelease <= 2 && input.remainingWorkHours > 0) {
    score += Math.min(20, input.remainingWorkHours / WORK_HOURS_PER_DAY * 4);
  }
  score += input.criticalBugCount * 25;
  score += input.blockedCount * 22;
  score += input.failedTestCount * 12;
  score += input.silentRiskCount * 8;
  if (input.remainingWorkHours > 0 && input.delayDays === 0) {
    score += Math.min(35, input.remainingWorkHours / WORK_HOURS_PER_DAY * 5);
  }
  return clampScore(score);
}

function getRiskLevel(score: number, delayDays: number, hasBlockingRisk: boolean): XiaobaoRiskLevel {
  if (hasBlockingRisk) return 'blocked';
  if (delayDays > 0 || score >= 75) return 'likely_delayed';
  if (score >= 55) return 'at_risk';
  if (score >= 30) return 'attention';
  return 'on_track';
}

function calcConfidence(input: {
  version: any;
  devTasks: any[];
  testCases: any[];
  bugs: any[];
  recentActivityCount: number;
}): number {
  let confidence = 100;
  if (!input.version.expectedReleaseDate) confidence -= 20;
  const workItems = [...input.devTasks, ...input.testCases, ...input.bugs];
  const missingEstimateCount = workItems.filter((item) => !hasEstimate(item)).length;
  if (missingEstimateCount > 0) confidence -= Math.min(25, missingEstimateCount * 5);
  if (input.testCases.length === 0) confidence -= 15;
  if (!Array.isArray(input.version.members) || input.version.members.length === 0) confidence -= 10;
  if (input.recentActivityCount === 0) confidence -= 10;
  confidence -= 10;
  return clampScore(confidence);
}

function hasEstimate(item: any): boolean {
  return (typeof item.estimateHours === 'number' && item.estimateHours > 0)
    || (typeof item.aiEstimateHours === 'number' && item.aiEstimateHours > 0);
}

function addWorkHours(start: Date, hours: number): Date {
  const result = new Date(start);
  result.setTime(result.getTime() + Math.ceil(hours / WORK_HOURS_PER_DAY) * 24 * 60 * 60 * 1000);
  return result;
}

function countRecentActivity(activities: any[], worklogs: any[], now: Date): number {
  const recentSince = now.getTime() - 3 * 86_400_000;
  return [
    ...activities.map((item) => item.occurredAt),
    ...worklogs.map((item) => item.createdAt),
  ].filter((value) => {
    const date = parseDate(value);
    return Boolean(date && date.getTime() >= recentSince);
  }).length;
}

function calcTodayWorklogHours(worklogs: any[], now: Date): number {
  const today = now.toISOString().slice(0, 10);
  return roundHours(worklogs
    .filter((worklog) => toIsoOrUndefined(worklog.workDate)?.slice(0, 10) === today)
    .reduce((sum, worklog) => sum + (Number(worklog.hours) || 0), 0));
}

function latestIso(values: Array<string | undefined>): string | undefined {
  let latest: string | undefined;
  let latestTime = Number.NEGATIVE_INFINITY;
  for (const value of values) {
    const date = parseDate(value);
    if (!date) continue;
    if (date.getTime() > latestTime) {
      latest = date.toISOString();
      latestTime = date.getTime();
    }
  }
  return latest;
}

function toIsoOrNull(value: unknown): string | null {
  return toIsoOrUndefined(value) ?? null;
}

function toIsoOrUndefined(value: unknown): string | undefined {
  const date = parseDate(value);
  return date?.toISOString();
}

function parseDate(value: unknown): Date | undefined {
  if (!value) return undefined;
  const date = value instanceof Date ? value : new Date(String(value));
  return Number.isFinite(date.getTime()) ? date : undefined;
}

function roundHours(hours: number): number {
  return Math.round(hours * 2) / 2;
}

function roundDays(days: number): number {
  return Math.round(days * 10) / 10;
}

function clampScore(score: number): number {
  return Math.max(0, Math.min(100, Math.round(score)));
}
