import { BadRequestException, Injectable } from '@nestjs/common';
import { RbacService } from '../../common/rbac/rbac.service';
import { PrismaService } from '../../prisma/prisma.service';

export interface ManagementOverviewQuery {
  actorId?: string;
  permissions?: string[];
  now?: Date;
}

type WorkItem = {
  id: string;
  versionId: string;
  title: string;
  ownerId?: string | null;
  assigneeId?: string | null;
  status?: string;
  isBlocked?: boolean;
  expectedEndAt?: Date | string | null;
  plannedEndAt?: Date | string | null;
  plannedFixAt?: Date | string | null;
};

@Injectable()
export class ManagementService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly rbacService: RbacService,
  ) {}

  async getOverview(query: ManagementOverviewQuery) {
    const actorId = query.actorId?.trim();
    if (!actorId) throw new BadRequestException('actorId is required');
    const permission = await this.rbacService.assertGlobalPermission({
      actorId,
      permissions: query.permissions ?? [],
      requiredPermissions: ['management:view'],
    });
    const now = query.now ?? new Date();
    const allProjects = permission.via === 'system';
    const projectIds = allProjects ? undefined : await this.getManagedProjectIds(actorId);
    if (!allProjects && projectIds?.length === 0) return emptyOverview();

    const activeVersions = await this.prisma.version.findMany({
      where: {
        ...(projectIds ? { projectId: { in: projectIds } } : {}),
        OR: [{ releaseDate: null }, { releaseDate: { gte: now } }],
      },
      orderBy: { releaseDate: 'asc' },
    });
    const versionIds = activeVersions.map((version: { id: string }) => version.id);
    if (versionIds.length === 0) {
      return { ...emptyOverview(), activeVersions };
    }

    const [versionPlans, devTasks, testCases, bugs, highRiskVersions] = await Promise.all([
      this.prisma.versionPlan.findMany({
        where: { versionId: { in: versionIds }, status: { not: 'completed' } },
      }),
      this.prisma.devTask.findMany({
        where: { versionId: { in: versionIds }, status: { not: 'submitted' } },
      }),
      this.prisma.testCase.findMany({
        where: { versionId: { in: versionIds }, status: { notIn: ['passed', 'failed', 'blocked'] } },
      }),
      this.prisma.bug.findMany({
        where: { versionId: { in: versionIds }, status: { in: ['open', 'fixing', 'fixed', 'verifying'] } },
      }),
      this.prisma.xiaobaoRiskSummary.findMany({
        where: { versionId: { in: versionIds }, riskLevel: { not: 'on_track' } },
        orderBy: [{ riskScore: 'desc' }, { updatedAt: 'desc' }],
      }),
    ]);

    const overdueItems = [
      ...collectOverdue(versionPlans, 'version_plan', now, 'expectedEndAt'),
      ...collectOverdue(devTasks, 'dev_task', now, 'expectedEndAt'),
      ...collectOverdue(testCases, 'test_case', now, 'plannedEndAt'),
      ...collectOverdue(bugs, 'bug', now, 'plannedFixAt'),
    ];
    const blockedItems = [
      ...devTasks.filter((item: WorkItem) => item.isBlocked).map((item: WorkItem) => toDashboardItem(item, 'dev_task')),
      ...testCases.filter((item: WorkItem) => item.status === 'blocked').map((item: WorkItem) => toDashboardItem(item, 'test_case')),
    ];

    return {
      activeVersionCount: activeVersions.length,
      overdueItemCount: overdueItems.length,
      blockedItemCount: blockedItems.length,
      riskCounts: countByRiskLevel(highRiskVersions),
      memberLoads: buildMemberLoads(versionPlans, devTasks, testCases, bugs),
      activeVersions,
      overdueItems,
      blockedItems,
      highRiskVersions,
    };
  }

  private async getManagedProjectIds(actorId: string): Promise<string[]> {
    const rows = await this.prisma.projectMember.findMany({
      where: { userId: actorId, role: { in: ['owner', 'admin'] } },
      select: { projectId: true },
    });
    return Array.from(new Set(rows.map((row: { projectId: string }) => row.projectId)));
  }
}

function emptyOverview() {
  return {
    activeVersionCount: 0,
    overdueItemCount: 0,
    blockedItemCount: 0,
    riskCounts: {},
    memberLoads: [],
    activeVersions: [],
    overdueItems: [],
    blockedItems: [],
    highRiskVersions: [],
  };
}

function collectOverdue(items: WorkItem[], type: string, now: Date, dateField: keyof WorkItem) {
  return items
    .filter((item) => isBefore(item[dateField], now))
    .map((item) => ({ ...toDashboardItem(item, type), dueAt: item[dateField] }));
}

function toDashboardItem(item: WorkItem, type: string) {
  return {
    type,
    id: item.id,
    versionId: item.versionId,
    title: item.title,
    ownerId: item.ownerId ?? item.assigneeId ?? null,
    status: item.status,
  };
}

function isBefore(value: unknown, now: Date): boolean {
  if (!value) return false;
  const time = value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
  return Number.isFinite(time) && time < now.getTime();
}

function countByRiskLevel(rows: Array<{ riskLevel: string }>): Record<string, number> {
  return rows.reduce<Record<string, number>>((acc, row) => {
    acc[row.riskLevel] = (acc[row.riskLevel] ?? 0) + 1;
    return acc;
  }, {});
}

function buildMemberLoads(...groups: WorkItem[][]) {
  const counts = new Map<string, number>();
  for (const item of groups.flat()) {
    const memberId = item.ownerId ?? item.assigneeId;
    if (!memberId) continue;
    counts.set(memberId, (counts.get(memberId) ?? 0) + 1);
  }
  return Array.from(counts.entries())
    .map(([memberId, openItemCount]) => ({ memberId, openItemCount }))
    .sort((a, b) => b.openItemCount - a.openItemCount || a.memberId.localeCompare(b.memberId));
}
