import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { APP_DATA_KEYS, isAppDataKey } from '../data/data-keys';
import { mapAppDataToV22Rows, type V22MappedRows } from './app-data-v22.mapper';
import { normalizeDateTimeFields } from './app-data-v22.migration.service';

type DelegateName =
  | 'user'
  | 'product'
  | 'project'
  | 'version'
  | 'taskCategory'
  | 'requirement'
  | 'versionPlan'
  | 'devTask'
  | 'testCase'
  | 'bug'
  | 'workActivity'
  | 'taskWorklog'
  | 'overtimeRecord'
  | 'xiaobaoRiskSnapshot'
  | 'xiaobaoRiskInsight'
  | 'xiaobaoRiskSummary';

type CreateManyDelegate = {
  createMany(args: { data: unknown[]; skipDuplicates: boolean }): Promise<{ count: number }>;
};

type DeleteManyDelegate = {
  deleteMany(args: { where: unknown }): Promise<{ count: number }>;
};

type UpsertDelegate = {
  upsert(args: unknown): Promise<unknown>;
};

type SyncDelegate = Partial<CreateManyDelegate & DeleteManyDelegate & UpsertDelegate>;
type SyncTransaction = Record<DelegateName, SyncDelegate>;

const RISK_INPUT_KEYS = new Set([
  'version-plans',
  'dev-tasks',
  'test-cases',
  'bugs',
  'work-activities',
  'task-worklogs',
  'overtime',
]);

@Injectable()
export class AppDataV23SyncService {
  constructor(private readonly prisma: PrismaService) {}

  async syncAfterAppDataPut(key: string): Promise<void> {
    if (!isAppDataKey(key)) return;

    const mapped = mapAppDataToV22Rows(await this.loadSnapshot());
    await this.prisma.$transaction(async (tx) => {
      const client = tx as unknown as SyncTransaction;
      await this.syncBaseTables(client, key, mapped);
      await this.syncCurrentStateTables(client, key, mapped);
      await this.syncAppendOnlyTables(client, key, mapped);
      await this.syncXiaobaoSummaries(client, key, mapped);
    });
  }

  private async loadSnapshot(): Promise<Record<string, unknown>> {
    const rows = await this.prisma.appData.findMany({
      where: { key: { in: [...APP_DATA_KEYS] } },
      select: { key: true, value: true },
    });
    return Object.fromEntries(rows.map((row) => [row.key, row.value]));
  }

  private async syncBaseTables(client: SyncTransaction, key: string, mapped: V22MappedRows) {
    if (key === 'products-overview') {
      await upsertById(client.product, mapped.products);
      await upsertById(client.project, mapped.projects);
      await upsertById(client.version, mapped.versions);
    }

    if (key === 'members') {
      await upsertById(client.user, mapped.users);
    }

    if (key === 'task-categories') {
      await upsertById(client.taskCategory, mapped.taskCategories);
    }
  }

  private async syncCurrentStateTables(client: SyncTransaction, key: string, mapped: V22MappedRows) {
    if (key === 'requirements') {
      const productIds = getProductScope(mapped);
      await replaceRows(
        client.requirement,
        { productId: { in: productIds } },
        mapped.requirements.filter((row) => productIds.includes(row.productId)),
      );
    }

    if (key === 'version-plans') {
      const versionIds = getVersionScope(mapped, mapped.versionPlans.map((row) => row.versionId));
      await replaceRows(
        client.versionPlan,
        { versionId: { in: versionIds } },
        mapped.versionPlans.filter((row) => versionIds.includes(row.versionId)),
      );
    }

    if (key === 'dev-tasks') {
      const versionIds = getVersionScope(mapped, mapped.devTasks.map((row) => row.versionId));
      await replaceRows(
        client.devTask,
        { versionId: { in: versionIds } },
        mapped.devTasks.filter((row) => versionIds.includes(row.versionId)),
      );
    }

    if (key === 'test-cases') {
      const versionIds = getVersionScope(mapped, mapped.testCases.map((row) => row.versionId));
      await replaceRows(
        client.testCase,
        { versionId: { in: versionIds } },
        mapped.testCases.filter((row) => versionIds.includes(row.versionId)),
      );
    }

    if (key === 'bugs') {
      const versionIds = getVersionScope(mapped, mapped.bugs.map((row) => row.versionId));
      await replaceRows(
        client.bug,
        { versionId: { in: versionIds } },
        mapped.bugs.filter((row) => versionIds.includes(row.versionId)),
      );
    }
  }

  private async syncAppendOnlyTables(client: SyncTransaction, key: string, mapped: V22MappedRows) {
    if (key === 'work-activities') {
      await createMany(client.workActivity, mapped.workActivities);
    }

    if (key === 'task-worklogs') {
      await createMany(client.taskWorklog, mapped.taskWorklogs);
    }

    if (key === 'overtime') {
      await createMany(client.overtimeRecord, mapped.overtimeRecords);
    }

    if (key === 'xiaobao-risk-snapshots') {
      await createMany(client.xiaobaoRiskSnapshot, mapped.xiaobaoRiskSnapshots);
    }

    if (key === 'xiaobao-risk-insights') {
      await createMany(client.xiaobaoRiskInsight, mapped.xiaobaoRiskInsights);
    }
  }

  private async syncXiaobaoSummaries(client: SyncTransaction, key: string, mapped: V22MappedRows) {
    if (key === 'xiaobao-risk-snapshots') {
      await upsertRiskSummaries(client.xiaobaoRiskSummary, mapped.xiaobaoRiskSummaries);
      return;
    }

    if (!RISK_INPUT_KEYS.has(key)) return;
    await markSummariesDirty(client.xiaobaoRiskSummary, getAffectedRiskVersionIds(key, mapped));
  }
}

async function upsertById(delegate: SyncDelegate, rows: Array<{ id: string }>) {
  if (!delegate.upsert) return;
  for (const row of rows) {
    const data = normalizeDateTimeFields(row);
    await delegate.upsert({
      where: { id: row.id },
      update: omitKeys(data, ['id']),
      create: data,
    });
  }
}

async function replaceRows(delegate: SyncDelegate, where: unknown, rows: unknown[]) {
  if (!delegate.deleteMany || !delegate.createMany) return;
  await delegate.deleteMany({ where });
  await createMany(delegate, rows);
}

async function createMany(delegate: SyncDelegate, rows: unknown[]) {
  if (!delegate.createMany || rows.length === 0) return;
  await delegate.createMany({
    data: rows.map(normalizeDateTimeFields),
    skipDuplicates: true,
  });
}

async function upsertRiskSummaries(delegate: SyncDelegate, rows: V22MappedRows['xiaobaoRiskSummaries']) {
  if (!delegate.upsert) return;
  for (const row of rows) {
    const data = normalizeDateTimeFields(row);
    await delegate.upsert({
      where: { versionId: row.versionId },
      update: omitKeys(data, ['versionId', 'updatedAt']),
      create: data,
    });
  }
}

async function markSummariesDirty(delegate: SyncDelegate, versionIds: string[]) {
  if (!delegate.upsert) return;
  for (const versionId of unique(versionIds)) {
    const riskSignature = `dirty:${versionId}`;
    await delegate.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,
      },
    });
  }
}

function getProductScope(mapped: V22MappedRows): string[] {
  return unique([
    ...mapped.products.map((row) => row.id),
    ...mapped.requirements.map((row) => row.productId),
  ]);
}

function getVersionScope(mapped: V22MappedRows, fallback: string[]): string[] {
  return unique([...mapped.versions.map((row) => row.id), ...fallback]);
}

function getAffectedRiskVersionIds(key: string, mapped: V22MappedRows): string[] {
  if (key === 'version-plans') return mapped.versionPlans.map((row) => row.versionId);
  if (key === 'dev-tasks') return mapped.devTasks.map((row) => row.versionId);
  if (key === 'test-cases') return mapped.testCases.map((row) => row.versionId);
  if (key === 'bugs') return mapped.bugs.map((row) => row.versionId);
  if (key === 'work-activities') return mapped.workActivities.map((row) => row.versionId).filter(isPresent);
  if (key === 'task-worklogs') return mapped.taskWorklogs.map((row) => row.versionId).filter(isPresent);
  if (key === 'overtime') return mapped.overtimeRecords.map((row) => row.versionId).filter(isPresent);
  return [];
}

function omitKeys(value: unknown, keys: string[]): unknown {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) return value;
  const blocked = new Set(keys);
  return Object.fromEntries(Object.entries(value).filter(([key]) => !blocked.has(key)));
}

function unique(values: string[]): string[] {
  return Array.from(new Set(values.filter(isPresent)));
}

function isPresent(value: string | undefined): value is string {
  return Boolean(value);
}
