import { BadRequestException, ConflictException, Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { AppDataV23SyncService } from '../migration/app-data-v23-sync.service';
import { isAppDataKey } from './data-keys';

type AppDataRow = {
  key: string;
  value: Prisma.JsonValue;
  updatedAt: Date;
};

@Injectable()
export class DataService {
  private readonly logger = new Logger(DataService.name);

  constructor(
    private prisma: PrismaService,
    private readonly appDataSync?: AppDataV23SyncService,
  ) {}

  async get(key: string) {
    this.ensureAllowedKey(key);
    const row = await this.prisma.appData.findUnique({ where: { key } });
    return this.toResponse(key, row);
  }

  async put(key: string, value: unknown, version?: string | null) {
    this.ensureAllowedKey(key);
    const jsonValue = value as Prisma.InputJsonValue;

    if (version === null) {
      try {
        const row = await this.prisma.appData.create({
          data: { key, value: jsonValue },
        });
        return this.toResponseAfterSync(key, row);
      } catch (error) {
        if (this.isUniqueConflict(error)) {
          await this.throwConflict(key);
        }
        throw error;
      }
    }

    if (version !== undefined) {
      const updatedAt = this.parseVersion(version);
      const result = await this.prisma.appData.updateMany({
        where: { key, updatedAt },
        data: { value: jsonValue },
      });

      if (result.count !== 1) {
        await this.throwConflict(key);
      }

      const row = await this.prisma.appData.findUnique({ where: { key } });
      return this.toResponseAfterSync(key, row);
    }

    const row = await this.prisma.appData.upsert({
      where: { key },
      update: { value: jsonValue },
      create: { key, value: jsonValue },
    });
    return this.toResponseAfterSync(key, row);
  }

  private ensureAllowedKey(key: string) {
    if (!isAppDataKey(key)) {
      throw new BadRequestException(`Unsupported data key: ${key}`);
    }
  }

  private parseVersion(version: string) {
    const updatedAt = new Date(version);
    if (!Number.isFinite(updatedAt.getTime())) {
      throw new BadRequestException('Invalid AppData version');
    }
    return updatedAt;
  }

  private toResponse(key: string, row: AppDataRow | null) {
    return {
      key,
      value: row?.value ?? null,
      version: row?.updatedAt.toISOString() ?? null,
    };
  }

  private async toResponseAfterSync(key: string, row: AppDataRow | null) {
    await this.syncRelations(key);
    return this.toResponse(key, row);
  }

  private async syncRelations(key: string) {
    if (!this.appDataSync) return;
    try {
      await this.appDataSync.syncAfterAppDataPut(key);
    } catch (error: any) {
      this.logger.warn(`AppData relation sync failed for ${key}: ${error?.message ?? error}`);
    }
  }

  private async throwConflict(key: string): Promise<never> {
    const row = await this.prisma.appData.findUnique({ where: { key } });
    const current = this.toResponse(key, row);
    throw new ConflictException({
      code: 'APP_DATA_CONFLICT',
      message: 'AppData was changed by another client. Reload before saving again.',
      key,
      currentValue: current.value,
      currentVersion: current.version,
    });
  }

  private isUniqueConflict(error: unknown) {
    return (
      typeof error === 'object' &&
      error !== null &&
      'code' in error &&
      (error as { code?: unknown }).code === 'P2002'
    );
  }
}
