import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '../generated/prisma/client';
import { EncryptionService } from '../infra/encryption.service';
import { PrismaService } from '../infra/prisma.service';
import { isProtectedSystemAccount } from './system-account.util';

export const DOCUMENT_TYPES = [
  'PASSPORT_RF',
  'FOREIGN_PASSPORT',
  'BIRTH_CERTIFICATE',
  'DRIVER_LICENSE',
  'VEHICLE_REGISTRATION',
  'OMS',
  'DMS',
  'INN',
  'SNILS'
] as const;

export type DocumentType = (typeof DOCUMENT_TYPES)[number];

interface CreateDocumentCommand {
  userId: string;
  type: string;
  number: string;
  issuedAt?: string;
  expiresAt?: string;
  metadataJson?: string;
}

interface UpdateDocumentCommand {
  documentId: string;
  userId: string;
  number?: string;
  issuedAt?: string;
  expiresAt?: string;
  metadataJson?: string;
}

@Injectable()
export class DocumentsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly encryption: EncryptionService
  ) {}

  private assertType(type: string): asserts type is DocumentType {
    if (!DOCUMENT_TYPES.includes(type as DocumentType)) {
      throw new BadRequestException('Некорректный тип документа');
    }
  }

  async create(command: CreateDocumentCommand) {
    this.assertType(command.type);

    const metadata = command.metadataJson
      ? ({ encryptedPayload: this.encryption.encrypt(command.metadataJson) } satisfies Prisma.InputJsonObject)
      : undefined;

    const document = await this.prisma.userDocument.create({
      data: {
        userId: command.userId,
        type: command.type,
        number: this.encryption.encrypt(command.number || '—'),
        issuedAt: command.issuedAt ? new Date(command.issuedAt) : undefined,
        expiresAt: command.expiresAt ? new Date(command.expiresAt) : undefined,
        metadata
      }
    });

    return this.toResponse(document);
  }

  async list(userId: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { isSystemAccount: true, linkedBotId: true }
    });
    if (user && isProtectedSystemAccount(user)) {
      return { documents: [] };
    }

    const documents = await this.prisma.userDocument.findMany({
      where: { userId },
      orderBy: { updatedAt: 'desc' }
    });

    const results = [];
    for (const document of documents) {
      try {
        results.push(this.toResponse(document));
      } catch {
        results.push(this.toFallbackResponse(document));
      }
    }

    return { documents: results };
  }

  async get(documentId: string, userId: string) {
    const document = await this.prisma.userDocument.findFirst({
      where: { id: documentId, userId }
    });
    if (!document) {
      throw new NotFoundException('Документ не найден');
    }
    return this.toResponse(document);
  }

  async update(command: UpdateDocumentCommand) {
    const existing = await this.prisma.userDocument.findFirst({
      where: { id: command.documentId, userId: command.userId }
    });
    if (!existing) {
      throw new NotFoundException('Документ не найден');
    }

    const metadata = command.metadataJson
      ? ({ encryptedPayload: this.encryption.encrypt(command.metadataJson) } satisfies Prisma.InputJsonObject)
      : undefined;

    const document = await this.prisma.userDocument.update({
      where: { id: command.documentId },
      data: {
        number: command.number ? this.encryption.encrypt(command.number) : undefined,
        issuedAt: command.issuedAt ? new Date(command.issuedAt) : undefined,
        expiresAt: command.expiresAt ? new Date(command.expiresAt) : undefined,
        metadata
      }
    });

    return this.toResponse(document);
  }

  async delete(documentId: string, userId: string) {
    const existing = await this.prisma.userDocument.findFirst({
      where: { id: documentId, userId }
    });
    if (!existing) {
      throw new NotFoundException('Документ не найден');
    }
    await this.prisma.userDocument.delete({ where: { id: documentId } });
    return { count: 1 };
  }

  private toResponse(document: Awaited<ReturnType<PrismaService['userDocument']['create']>>) {
    const response: Record<string, string> = {
      id: document.id,
      userId: document.userId,
      type: document.type,
      number: this.encryption.decryptIfNeeded(document.number ?? '—'),
      createdAt: this.formatRequiredDate(document.createdAt),
      updatedAt: this.formatRequiredDate(document.updatedAt)
    };

    const issuedAt = this.formatOptionalDate(document.issuedAt);
    if (issuedAt) response.issuedAt = issuedAt;

    const expiresAt = this.formatOptionalDate(document.expiresAt);
    if (expiresAt) response.expiresAt = expiresAt;

    const metadataJson = this.resolveMetadataJson(document.metadata);
    if (metadataJson) response.metadataJson = metadataJson;

    return response;
  }

  private toFallbackResponse(document: Awaited<ReturnType<PrismaService['userDocument']['create']>>) {
    return {
      id: document.id,
      userId: document.userId,
      type: document.type,
      number: '—',
      createdAt: this.formatRequiredDate(document.createdAt),
      updatedAt: this.formatRequiredDate(document.updatedAt)
    };
  }

  private resolveMetadataJson(metadata: Prisma.JsonValue | null): string | undefined {
    if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
      return undefined;
    }

    const record = metadata as Record<string, unknown>;
    if (typeof record.encryptedPayload === 'string') {
      return this.encryption.decryptIfNeeded(record.encryptedPayload);
    }

    if (Object.keys(record).length === 0) {
      return undefined;
    }

    return JSON.stringify(record);
  }

  private formatOptionalDate(value: Date | null | undefined): string | undefined {
    if (!value || Number.isNaN(value.getTime())) {
      return undefined;
    }
    return value.toISOString();
  }

  private formatRequiredDate(value: Date): string {
    if (Number.isNaN(value.getTime())) {
      return new Date(0).toISOString();
    }
    return value.toISOString();
  }
}
