import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { AppReleasePlatform, Prisma } from '../generated/prisma/client';
import { PrismaService } from '../infra/prisma.service';
import { MinioService } from '../infra/minio.service';

type PlatformInput = 'ANDROID' | 'WINDOWS';

@Injectable()
export class AppReleaseService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly minio: MinioService
  ) {}

  private normalizePlatform(platform: string): AppReleasePlatform {
    const normalized = platform.trim().toUpperCase();
    if (normalized === 'ANDROID' || normalized === 'WINDOWS') {
      return normalized;
    }
    throw new BadRequestException('Платформа должна быть ANDROID или WINDOWS');
  }

  private toResponse(release: {
    id: string;
    platform: AppReleasePlatform;
    version: string;
    versionCode: number;
    fileName: string;
    storageKey: string;
    fileSize: bigint;
    sha256: string;
    releaseNotes: string | null;
    isPublished: boolean;
    createdAt: Date;
    updatedAt: Date;
  }) {
    return {
      id: release.id,
      platform: release.platform,
      version: release.version,
      versionCode: release.versionCode,
      fileName: release.fileName,
      storageKey: release.storageKey,
      fileSize: release.fileSize.toString(),
      sha256: release.sha256,
      releaseNotes: release.releaseNotes ?? undefined,
      isPublished: release.isPublished,
      createdAt: release.createdAt.toISOString(),
      updatedAt: release.updatedAt.toISOString()
    };
  }

  private contentTypeForPlatform(platform: AppReleasePlatform, fileName: string) {
    const lower = fileName.toLowerCase();
    if (platform === AppReleasePlatform.ANDROID || lower.endsWith('.apk')) {
      return 'application/vnd.android.package-archive';
    }
    if (platform === AppReleasePlatform.WINDOWS || lower.endsWith('.exe')) {
      return 'application/vnd.microsoft.portable-executable';
    }
    return 'application/octet-stream';
  }

  assertReleaseFile(platform: AppReleasePlatform, fileName: string) {
    const lower = fileName.trim().toLowerCase();
    if (platform === AppReleasePlatform.ANDROID && !lower.endsWith('.apk')) {
      throw new BadRequestException('Для Android загрузите файл .apk');
    }
    if (platform === AppReleasePlatform.WINDOWS && !lower.endsWith('.exe')) {
      throw new BadRequestException('Для Windows загрузите файл .exe');
    }
  }

  buildStorageKey(platform: AppReleasePlatform, version: string, fileName: string) {
    const safeVersion = version.replace(/[^a-zA-Z0-9._-]+/g, '_');
    const safeName = fileName.replace(/[^a-zA-Z0-9._-]+/g, '_');
    return `releases/${platform.toLowerCase()}/${safeVersion}/${randomUUID()}-${safeName}`;
  }

  async listReleases(platform?: string) {
    const where: Prisma.AppReleaseWhereInput = platform
      ? { platform: this.normalizePlatform(platform) }
      : {};
    const releases = await this.prisma.appRelease.findMany({
      where,
      orderBy: [{ platform: 'asc' }, { versionCode: 'desc' }, { createdAt: 'desc' }]
    });
    return { releases: releases.map((release) => this.toResponse(release)) };
  }

  async listPublicReleases(platform?: string) {
    const where: Prisma.AppReleaseWhereInput = {
      isPublished: true,
      ...(platform ? { platform: this.normalizePlatform(platform) } : {})
    };
    const releases = await this.prisma.appRelease.findMany({
      where,
      orderBy: [{ platform: 'asc' }, { versionCode: 'desc' }, { createdAt: 'desc' }]
    });
    return { releases: releases.map((release) => this.toResponse(release)) };
  }

  async createRelease(input: {
    platform: string;
    version: string;
    versionCode: number;
    fileName: string;
    storageKey: string;
    fileSize: string | number | bigint;
    sha256: string;
    releaseNotes?: string;
    createdById?: string;
  }) {
    const platform = this.normalizePlatform(input.platform);
    const version = input.version.trim();
    if (!/^\d+(?:\.\d+){0,3}(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
      throw new BadRequestException('Версия должна быть в формате semver, например 1.2.3');
    }
    if (!Number.isInteger(input.versionCode) || input.versionCode < 1) {
      throw new BadRequestException('Код версии должен быть положительным целым числом');
    }
    const sha256 = input.sha256.trim().toLowerCase();
    if (!/^[a-f0-9]{64}$/.test(sha256)) {
      throw new BadRequestException('Некорректная SHA-256 подпись файла');
    }

    this.assertReleaseFile(platform, input.fileName);

    if (!input.storageKey.startsWith(`releases/${platform.toLowerCase()}/`)) {
      throw new BadRequestException('Некорректный ключ хранения релиза');
    }

    const exists = await this.minio.objectExists(input.storageKey);
    if (!exists) {
      throw new BadRequestException('Файл релиза не найден в хранилище. Повторите загрузку.');
    }

    try {
      const release = await this.prisma.appRelease.create({
        data: {
          platform,
          version,
          versionCode: input.versionCode,
          fileName: input.fileName.trim(),
          storageKey: input.storageKey,
          fileSize: BigInt(input.fileSize),
          sha256,
          releaseNotes: input.releaseNotes?.trim() || null,
          createdById: input.createdById ?? null
        }
      });
      return this.toResponse(release);
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
        throw new BadRequestException('Релиз с такой версией или кодом версии уже существует для этой платформы');
      }
      throw error;
    }
  }

  async updateRelease(releaseId: string, patch: { isPublished?: boolean; releaseNotes?: string }) {
    const release = await this.prisma.appRelease.update({
      where: { id: releaseId },
      data: {
        ...(patch.isPublished !== undefined ? { isPublished: patch.isPublished } : {}),
        ...(patch.releaseNotes !== undefined ? { releaseNotes: patch.releaseNotes.trim() || null } : {})
      }
    });
    return this.toResponse(release);
  }

  async deleteRelease(releaseId: string) {
    const release = await this.prisma.appRelease.findUnique({ where: { id: releaseId } });
    if (!release) {
      throw new NotFoundException('Релиз не найден');
    }

    await this.prisma.appRelease.delete({ where: { id: releaseId } });
    await this.minio.deleteObject(release.storageKey).catch(() => undefined);
    return { deleted: true };
  }

  async getLatestRelease(platform: string) {
    const normalized = this.normalizePlatform(platform);
    const release = await this.prisma.appRelease.findFirst({
      where: { platform: normalized, isPublished: true },
      orderBy: [{ versionCode: 'desc' }, { createdAt: 'desc' }]
    });
    if (!release) {
      throw new NotFoundException('Опубликованный релиз для этой платформы не найден');
    }
    return this.toResponse(release);
  }

  async checkUpdate(platform: string, versionCode: number) {
    const latest = await this.getLatestRelease(platform);
    const updateAvailable = versionCode < latest.versionCode;
    return {
      updateAvailable,
      latest: updateAvailable ? latest : undefined
    };
  }

  async resolveDownload(platform: string, releaseId?: string) {
    const normalized = this.normalizePlatform(platform);
    const release = releaseId
      ? await this.prisma.appRelease.findFirst({
          where: { id: releaseId, platform: normalized, isPublished: true }
        })
      : await this.prisma.appRelease.findFirst({
          where: { platform: normalized, isPublished: true },
          orderBy: [{ versionCode: 'desc' }, { createdAt: 'desc' }]
        });

    if (!release) {
      throw new NotFoundException('Файл релиза не найден');
    }

    return {
      storageKey: release.storageKey,
      fileName: release.fileName,
      contentType: this.contentTypeForPlatform(release.platform, release.fileName),
      fileSize: release.fileSize.toString(),
      sha256: release.sha256,
      version: release.version,
      versionCode: release.versionCode
    };
  }
}
