import type { TaskWorklog } from './task-worklog';
import type { WorkActivity, WorkActivityCategory } from './work-activity';
import type { WorkItem } from './workspace-engine';
import { calcActualElapsedHours } from './work-hours';

export interface WorkspaceDailyReportItem {
  id: string;
  taskId: string;
  taskTitle: string;
  workContent: string;
  hours: number;
  createdAt: string;
  productName?: string;
  projectName?: string;
  versionName?: string;
}

export interface WorkspaceDailyReportActivityItem {
  id: string;
  sourceId: string;
  sourceType: WorkActivity['sourceType'];
  action: WorkActivity['action'];
  category: WorkActivityCategory;
  title: string;
  summary: string;
  occurredAt: string;
  context?: string;
  metadata?: WorkActivity['metadata'];
}

export interface WorkspaceDailyReportNeedsProgressItem {
  id: string;
  title: string;
  status: string;
  type: WorkItem['type'];
  context?: string;
}

export type WorkspaceDailyReportGroups = Record<WorkActivityCategory, WorkspaceDailyReportActivityItem[]>;

export interface WorkspaceDailyReport {
  date: string;
  totalHours: number;
  totalCount: number;
  items: WorkspaceDailyReportItem[];
  groups: WorkspaceDailyReportGroups;
  needsProgressItems: WorkspaceDailyReportNeedsProgressItem[];
}

interface GetWorkspaceDailyReportInput {
  activities?: WorkActivity[];
  worklogs: TaskWorklog[];
  workItems: WorkItem[];
  userId: string;
  date: string;
  now?: Date;
}

export function getWorkspaceDailyReport({
  activities = [],
  worklogs,
  workItems,
  userId,
  date,
  now = new Date(),
}: GetWorkspaceDailyReportInput): WorkspaceDailyReport {
  const workItemMap = new Map(workItems.map((item) => [item.id, item]));

  const items = worklogs
    .filter((log) => log.userId === userId && log.date === date)
    .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
    .map((log) => {
      const item = workItemMap.get(log.taskId);

      return {
        id: log.id,
        taskId: log.taskId,
        taskTitle: item?.title ?? '未知任务',
        workContent: log.workContent,
        hours: log.hours,
        createdAt: log.createdAt,
        productName: item?.productName,
        projectName: item?.projectName,
        versionName: item?.versionName,
      };
    });

  const userActivities = activities
    .filter((activity) => activity.actorId === userId)
    .sort((a, b) => a.occurredAt.localeCompare(b.occurredAt));
  const dailyActivities = userActivities
    .filter((activity) => activity.date === date)
    .sort((a, b) => b.occurredAt.localeCompare(a.occurredAt));
  const activityKeys = new Set(dailyActivities.map(getActivityKey));
  const evidenceSourceIds = new Set([
    ...dailyActivities.map((activity) => activity.sourceId),
    ...items.map((item) => item.taskId),
  ]);
  const reportActivities = [
    ...dailyActivities,
    ...buildTimestampFallbackActivities(workItems, userId, date, activityKeys, evidenceSourceIds),
  ].sort((a, b) => b.occurredAt.localeCompare(a.occurredAt));

  const groups = makeEmptyGroups();
  for (const activity of reportActivities) {
    const item = workItemMap.get(activity.sourceId);
    groups[activity.category].push({
      id: activity.id,
      sourceId: activity.sourceId,
      sourceType: activity.sourceType,
      action: activity.action,
      category: activity.category,
      title: activity.title,
      summary: activity.summary,
      occurredAt: activity.occurredAt,
      context: getContext(item),
      metadata: activity.metadata,
    });
  }

  const touchedWorkItemIds = buildTouchedWorkItemIds(reportActivities, items, workItems);
  const worklogTaskIds = new Set(items.map((item) => item.taskId));
  const activityHours = calcActivityHoursForDate(
    reportActivities,
    userActivities,
    workItemMap,
    worklogTaskIds,
    date,
    now,
  );

  const needsProgressItems = workItems
    .filter((item) => shouldRequireProgress(item, date, touchedWorkItemIds))
    .map((item) => ({
      id: item.id,
      title: item.title,
      status: item.status,
      type: item.type,
      context: getContext(item),
    }));

  return {
    date,
    totalHours: roundHalfHour(items.reduce((sum, item) => sum + item.hours, 0) + activityHours),
    totalCount: items.length + reportActivities.length,
    items,
    groups,
    needsProgressItems,
  };
}

function makeEmptyGroups(): WorkspaceDailyReportGroups {
  return {
    delivery: [],
    progress: [],
    creation: [],
    risk: [],
    note: [],
  };
}

function getContext(item?: WorkItem): string | undefined {
  if (!item) return undefined;
  return [item.productName, item.projectName, item.versionName].filter(Boolean).join(' / ');
}

function getActivityKey(activity: Pick<WorkActivity, 'sourceId' | 'action'>): string {
  return `${activity.sourceId}:${activity.action}`;
}

function getRaw(item: WorkItem): Record<string, unknown> {
  return ((item.raw ?? {}) as unknown) as Record<string, unknown>;
}

function buildTouchedWorkItemIds(
  activities: WorkActivity[],
  items: WorkspaceDailyReportItem[],
  workItems: WorkItem[],
): Set<string> {
  const touched = new Set([
    ...activities.map((activity) => activity.sourceId),
    ...items.map((item) => item.taskId),
  ]);
  const touchedRequirementIds = new Set(
    activities
      .map(getPlanProgressRequirementId)
      .filter((requirementId): requirementId is string => Boolean(requirementId)),
  );

  if (touchedRequirementIds.size === 0) return touched;

  for (const item of workItems) {
    const requirementId = getWorkItemRequirementId(item);
    if (requirementId && touchedRequirementIds.has(requirementId)) {
      touched.add(item.id);
    }
  }

  return touched;
}

function getPlanProgressRequirementId(activity: WorkActivity): string | undefined {
  if (activity.sourceType !== 'version_plan' || activity.action !== 'version_plan_requirement_progress') return undefined;
  return asString(activity.metadata?.requirementId);
}

function getWorkItemRequirementId(item: WorkItem): string | undefined {
  const raw = getRaw(item);
  return asString(raw.requirementId) ?? asString(item.extra?.requirementId);
}

function shouldRequireProgress(item: WorkItem, date: string, touchedWorkItemIds: Set<string>): boolean {
  if (item.completed) return false;
  if (touchedWorkItemIds.has(item.id)) return false;
  if (!isInProgressStatus(item.status)) return false;

  const startedAt = getStartedAt(item);
  if (!startedAt) return false;
  return startedAt.slice(0, 10) < date;
}

function isInProgressStatus(status: string): boolean {
  return ['in_progress', 'testing', 'running', 'fixing', 'verifying'].includes(status);
}

function getStartedAt(item: WorkItem): string | undefined {
  const extra = item.extra ?? {};
  if (typeof extra.actualStartAt === 'string') return extra.actualStartAt;
  if (typeof extra.startedAt === 'string') return extra.startedAt;
  if (typeof extra.startTime === 'string') return extra.startTime;
  return undefined;
}

function buildTimestampFallbackActivities(
  workItems: WorkItem[],
  userId: string,
  date: string,
  existingKeys: Set<string>,
  evidenceSourceIds: Set<string>,
): WorkActivity[] {
  const out: WorkActivity[] = [];

  for (const item of workItems) {
    if (evidenceSourceIds.has(item.id)) continue;
    for (const draft of getTimestampFallbackDrafts(item)) {
      if (!isWithinLocalDate(draft.occurredAt, date)) continue;
      const activity: WorkActivity = {
        id: `fallback-${draft.action}-${item.id}`,
        actorId: userId,
        date,
        sourceType: draft.sourceType,
        sourceId: item.id,
        action: draft.action,
        category: draft.category,
        title: item.title,
        summary: draft.summary,
        occurredAt: draft.occurredAt,
      };
      const key = getActivityKey(activity);
      if (existingKeys.has(key)) continue;
      existingKeys.add(key);
      out.push(activity);
    }
  }

  return out;
}

function getTimestampFallbackDrafts(item: WorkItem): Array<Pick<WorkActivity, 'sourceType' | 'action' | 'category' | 'summary' | 'occurredAt'>> {
  const raw = getRaw(item);
  const extra = item.extra ?? {};

  if (item.type === 'plan_research' || item.type === 'plan_product' || item.type === 'plan_ui') {
    return [
      makeFallbackDraft('version_plan', 'version_plan_created', 'creation', asString(raw.createdAt), `新建计划：${item.title}`),
      makeFallbackDraft('version_plan', 'version_plan_started', 'progress', asString(raw.actualStartAt) ?? asString(extra.actualStartAt), `开始计划：${item.title}`),
      makeFallbackDraft('version_plan', 'version_plan_completed', 'delivery', asString(raw.completedAt), `完成计划：${item.title}`),
    ].filter(isFallbackDraft);
  }

  if (item.type === 'devTask') {
    return [
      makeFallbackDraft('dev_task', 'dev_task_created', 'creation', asString(raw.createdAt), `新建开发任务：${item.title}`),
      makeFallbackDraft('dev_task', 'dev_task_started', 'progress', asString(raw.actualStartAt) ?? asString(extra.actualStartAt), `开始开发：${item.title}`),
      makeFallbackDraft('dev_task', 'dev_task_submitted', 'delivery', asString(raw.actualEndAt) ?? asString(extra.actualEndAt), `已提测开发任务：${item.title}`),
    ].filter(isFallbackDraft);
  }

  if (item.type === 'testCase') {
    const completedAt = asString(raw.completedAt) ?? asString(extra.completedAt);
    const terminal = getTestCaseTerminalFallback(item.status, item.title, completedAt);
    return [
      makeFallbackDraft('test_case', 'test_case_created', 'creation', asString(raw.createdAt), `新建测试用例：${item.title}`),
      makeFallbackDraft('test_case', 'test_case_started', 'progress', asString(raw.startedAt) ?? asString(extra.startedAt), `开始测试：${item.title}`),
      terminal,
    ].filter(isFallbackDraft);
  }

  if (item.type === 'bug') {
    return [
      makeFallbackDraft('bug', 'bug_created', 'creation', asString(raw.createdAt), `新建 Bug：${item.title}`),
      makeFallbackDraft('bug', 'bug_fixing', 'progress', getBugFixingStartedAt(raw), `开始修复 Bug：${item.title}`),
      makeFallbackDraft('bug', 'bug_fixed', 'delivery', asString(raw.resolvedAt), `已修复 Bug：${item.title}`),
      makeFallbackDraft('bug', 'bug_closed', 'delivery', asString(raw.closedAt), `已关闭 Bug：${item.title}`),
    ].filter(isFallbackDraft);
  }

  return [];
}

function makeFallbackDraft(
  sourceType: WorkActivity['sourceType'],
  action: WorkActivity['action'],
  category: WorkActivityCategory,
  occurredAt: string | undefined,
  summary: string,
): Pick<WorkActivity, 'sourceType' | 'action' | 'category' | 'summary' | 'occurredAt'> | undefined {
  if (!occurredAt) return undefined;
  return { sourceType, action, category, summary, occurredAt };
}

function isFallbackDraft(
  draft: Pick<WorkActivity, 'sourceType' | 'action' | 'category' | 'summary' | 'occurredAt'> | undefined,
): draft is Pick<WorkActivity, 'sourceType' | 'action' | 'category' | 'summary' | 'occurredAt'> {
  return Boolean(draft);
}

function getTestCaseTerminalFallback(
  status: string,
  title: string,
  completedAt: string | undefined,
): Pick<WorkActivity, 'sourceType' | 'action' | 'category' | 'summary' | 'occurredAt'> | undefined {
  if (status === 'passed') return makeFallbackDraft('test_case', 'test_case_passed', 'delivery', completedAt, `测试通过：${title}`);
  if (status === 'failed') return makeFallbackDraft('test_case', 'test_case_failed', 'risk', completedAt, `测试不通过：${title}`);
  if (status === 'blocked') return makeFallbackDraft('test_case', 'test_case_blocked', 'risk', completedAt, `测试阻塞：${title}`);
  return undefined;
}

function calcActivityHoursForDate(
  activities: WorkActivity[],
  allUserActivities: WorkActivity[],
  workItemMap: Map<string, WorkItem>,
  excludedSourceIds: Set<string>,
  date: string,
  now: Date,
): number {
  const recordScopedSourceIds = new Set<string>();
  const intervals = activities
    .filter((activity) => !excludedSourceIds.has(activity.sourceId))
    .map((activity) => {
      if (!isRecordScopedProgressActivity(activity)) return undefined;
      recordScopedSourceIds.add(activity.sourceId);
      const workStartedAt = getRecordScopedWorkStartedAt(activity, allUserActivities, workItemMap, date);
      if (!workStartedAt) return undefined;
      return getActivityIntervalForDate(workStartedAt, activity.occurredAt, date);
    })
    .filter(isTimeInterval);

  const sourceIds = Array.from(new Set(activities.map((activity) => activity.sourceId)));
  const fallbackIntervals = sourceIds
    .filter((sourceId) => !excludedSourceIds.has(sourceId))
    .filter((sourceId) => !recordScopedSourceIds.has(sourceId))
    .map((sourceId) => getWorkItemIntervalForDate(workItemMap.get(sourceId), date, now))
    .filter(isTimeInterval);

  return calcMergedIntervalHours([...intervals, ...fallbackIntervals]);
}

function isPlanRecordProgressActivity(activity: WorkActivity): boolean {
  return activity.sourceType === 'version_plan'
    && (activity.action === 'version_plan_requirement_progress'
      || activity.action === 'version_plan_research_direction_progress');
}

function isProgressNoteRecordActivity(activity: WorkActivity): boolean {
  return activity.action === 'progress_note_added' && Boolean(asString(activity.metadata?.nextStartAt));
}

function isRecordScopedProgressActivity(activity: WorkActivity): boolean {
  return isPlanRecordProgressActivity(activity) || isProgressNoteRecordActivity(activity);
}

function getRecordScopedWorkStartedAt(
  activity: WorkActivity,
  allUserActivities: WorkActivity[],
  workItemMap: Map<string, WorkItem>,
  date: string,
): string | undefined {
  if (isPlanRecordProgressActivity(activity)) {
    return asString(activity.metadata?.workStartedAt)
      ?? getPreviousRecordNextStartAt(activity, allUserActivities)
      ?? getSameDayWorkItemStartAt(workItemMap.get(activity.sourceId), date);
  }

  if (isProgressNoteRecordActivity(activity)) {
    return getPreviousRecordNextStartAt(activity, allUserActivities)
      ?? getSameDayWorkItemStartAt(workItemMap.get(activity.sourceId), date);
  }

  return undefined;
}

function getPreviousRecordNextStartAt(activity: WorkActivity, allUserActivities: WorkActivity[]): string | undefined {
  const currentTime = new Date(activity.occurredAt).getTime();
  if (!Number.isFinite(currentTime)) return undefined;

  return [...allUserActivities]
    .filter((item) => item.sourceId === activity.sourceId && item.occurredAt < activity.occurredAt)
    .sort((a, b) => b.occurredAt.localeCompare(a.occurredAt))
    .map((item) => asString(item.metadata?.nextStartAt))
    .find((nextStartAt) => {
      if (!nextStartAt) return false;
      const startTime = new Date(nextStartAt).getTime();
      return Number.isFinite(startTime) && startTime < currentTime;
    });
}

function getSameDayWorkItemStartAt(item: WorkItem | undefined, date: string): string | undefined {
  if (!item) return undefined;
  const startAt = getStartedAt(item);
  if (!startAt || !isWithinLocalDate(startAt, date)) return undefined;
  return startAt;
}

function getActivityIntervalForDate(
  startAt: string,
  occurredAt: string,
  date: string,
): { start: number; end: number } | undefined {
  const day = getLocalDateBounds(date);
  if (!day) return undefined;

  const start = new Date(startAt).getTime();
  const end = new Date(occurredAt).getTime();
  if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return undefined;

  const overlapStart = Math.max(start, day.start.getTime());
  const overlapEnd = Math.min(end, day.end.getTime());
  if (overlapEnd <= overlapStart) return undefined;

  return { start: overlapStart, end: overlapEnd };
}

function getWorkItemIntervalForDate(item: WorkItem | undefined, date: string, now: Date): { start: number; end: number } | undefined {
  const interval = getActualInterval(item, now);
  if (!interval) return undefined;

  const day = getLocalDateBounds(date);
  if (!day) return undefined;

  const start = new Date(interval.start).getTime();
  const end = new Date(interval.end).getTime();
  if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return undefined;

  const overlapStart = Math.max(start, day.start.getTime());
  const overlapEnd = Math.min(end, day.end.getTime());
  if (overlapEnd <= overlapStart) return undefined;

  return { start: overlapStart, end: overlapEnd };
}

function isTimeInterval(interval: { start: number; end: number } | undefined): interval is { start: number; end: number } {
  return Boolean(interval);
}

function calcMergedIntervalHours(intervals: Array<{ start: number; end: number }>): number {
  if (intervals.length === 0) return 0;

  const sorted = [...intervals].sort((a, b) => a.start - b.start);
  const merged: Array<{ start: number; end: number }> = [];
  let current = { ...sorted[0] };

  for (const interval of sorted.slice(1)) {
    if (interval.start <= current.end) {
      current.end = Math.max(current.end, interval.end);
    } else {
      merged.push(current);
      current = { ...interval };
    }
  }
  merged.push(current);

  return merged.reduce((sum, interval) => sum + calcActualElapsedHours(
    new Date(interval.start).toISOString(),
    new Date(interval.end).toISOString(),
  ), 0);
}

function getActualInterval(item: WorkItem | undefined, now: Date): { start: string; end: string } | undefined {
  if (!item) return undefined;
  const raw = getRaw(item);
  const extra = item.extra ?? {};
  const nowIso = now.toISOString();

  if (item.type === 'plan_research' || item.type === 'plan_product' || item.type === 'plan_ui') {
    const start = asString(raw.actualStartAt) ?? asString(extra.actualStartAt);
    const end = asString(raw.completedAt) ?? nowIso;
    return start ? { start, end } : undefined;
  }

  if (item.type === 'devTask') {
    const start = asString(raw.actualStartAt) ?? asString(extra.actualStartAt);
    const end = asString(raw.actualEndAt) ?? asString(extra.actualEndAt) ?? (item.status === 'submitted' ? asString(raw.updatedAt) : undefined) ?? nowIso;
    return start ? { start, end } : undefined;
  }

  if (item.type === 'testCase') {
    const start = asString(raw.startedAt) ?? asString(extra.startedAt);
    const isTerminal = ['passed', 'failed', 'blocked'].includes(item.status);
    const end = asString(raw.completedAt) ?? asString(extra.completedAt) ?? (isTerminal ? asString(raw.updatedAt) : undefined) ?? nowIso;
    return start ? { start, end } : undefined;
  }

  if (item.type === 'bug') {
    const start = getBugFixingStartedAt(raw) ?? asString(raw.createdAt) ?? asString(extra.createdAt);
    const isTerminal = ['closed', 'rejected'].includes(item.status);
    const end = asString(raw.closedAt) ?? asString(raw.resolvedAt) ?? (isTerminal ? asString(raw.updatedAt) : undefined) ?? nowIso;
    return start ? { start, end } : undefined;
  }

  return undefined;
}

function getBugFixingStartedAt(raw: Record<string, unknown>): string | undefined {
  const logs = raw.logs;
  if (!Array.isArray(logs)) return undefined;
  const fixingLog = logs.find((log) => {
    if (!log || typeof log !== 'object') return false;
    const row = log as Record<string, unknown>;
    return row.action === 'status_change' && row.toValue === 'fixing' && typeof row.createdAt === 'string';
  }) as Record<string, unknown> | undefined;
  return asString(fixingLog?.createdAt);
}

function getLocalDateBounds(date: string): { start: Date; end: Date } | undefined {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
  if (!match) return undefined;
  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return undefined;
  const start = new Date(year, month - 1, day, 0, 0, 0, 0);
  const end = new Date(year, month - 1, day + 1, 0, 0, 0, 0);
  if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime())) return undefined;
  return { start, end };
}

function isWithinLocalDate(value: string, date: string): boolean {
  const bounds = getLocalDateBounds(date);
  if (!bounds) return false;
  const time = new Date(value).getTime();
  if (!Number.isFinite(time)) return false;
  return time >= bounds.start.getTime() && time < bounds.end.getTime();
}

function asString(value: unknown): string | undefined {
  return typeof value === 'string' && value.length > 0 ? value : undefined;
}

function roundHalfHour(hours: number): number {
  if (!Number.isFinite(hours) || hours <= 0) return 0;
  return Math.round(hours * 2) / 2;
}
