import { Injectable, Logger } from '@nestjs/common'
import * as unzipper from 'unzipper'
import * as bplist from 'bplist-parser'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'
import { Platform } from '@/domain/enums/platform.enum'

// eslint-disable-next-line @typescript-eslint/no-require-imports
const ApkReader = require('adbkit-apkreader')

export interface ParsedPackageInfo {
  packageName: string
  versionCode: number
  versionName: string
  minSdkVersion?: string
  targetSdkVersion?: string
}

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

  async parsePackage(buffer: Buffer, platform: Platform): Promise<ParsedPackageInfo | null> {
    try {
      if (platform === Platform.ANDROID) {
        return await this.parseApk(buffer)
      } else if (platform === Platform.IOS) {
        return await this.parseIpa(buffer)
      }
      return null
    } catch (error) {
      this.logger.error(`Failed to parse package: ${error.message}`, error.stack)
      return null
    }
  }

  private async parseApk(buffer: Buffer): Promise<ParsedPackageInfo | null> {
    // adbkit-apkreader only supports file paths, not buffers
    // So we need to write buffer to a temp file first
    const tempFile = path.join(os.tmpdir(), `apk-parse-${Date.now()}.apk`)

    try {
      // Write buffer to temp file
      await fs.writeFile(tempFile, buffer)

      // Open APK from file path
      const reader = await ApkReader.open(tempFile)
      const manifest = await reader.readManifest()

      return {
        packageName: manifest.package,
        versionCode: manifest.versionCode,
        versionName: manifest.versionName,
        minSdkVersion: manifest.usesSdk?.minSdkVersion?.toString(),
        targetSdkVersion: manifest.usesSdk?.targetSdkVersion?.toString(),
      }
    } catch (error) {
      this.logger.error(`Failed to parse APK: ${error.message}`)
      throw error
    } finally {
      // Clean up temp file
      try {
        await fs.unlink(tempFile)
      } catch {
        // Ignore cleanup errors
      }
    }
  }

  private async parseIpa(buffer: Buffer): Promise<ParsedPackageInfo | null> {
    try {
      const directory = await unzipper.Open.buffer(buffer)

      // Find Info.plist in the .app directory
      const plistFile = directory.files.find(
        (file: { path: string }) => file.path.match(/Payload\/[^/]+\.app\/Info\.plist$/)
      )

      if (!plistFile) {
        throw new Error('Info.plist not found in IPA')
      }

      const plistBuffer = await plistFile.buffer()

      // Try to parse as binary plist first
      let plistData: any
      try {
        const parsed = bplist.parseBuffer(plistBuffer)
        plistData = parsed[0]
      } catch {
        // If binary parsing fails, try XML parsing
        const plistContent = plistBuffer.toString('utf8')
        plistData = this.parseXmlPlist(plistContent)
      }

      if (!plistData) {
        throw new Error('Failed to parse Info.plist')
      }

      return {
        packageName: plistData.CFBundleIdentifier || '',
        versionCode: parseInt(plistData.CFBundleVersion || '0', 10),
        versionName: plistData.CFBundleShortVersionString || plistData.CFBundleVersion || '',
        minSdkVersion: plistData.MinimumOSVersion,
      }
    } catch (error) {
      this.logger.error(`Failed to parse IPA: ${error.message}`)
      throw error
    }
  }

  private parseXmlPlist(content: string): Record<string, any> | null {
    try {
      const result: Record<string, any> = {}
      const keyRegex = /<key>([^<]+)<\/key>\s*<string>([^<]+)<\/string>/g
      let match

      while ((match = keyRegex.exec(content)) !== null) {
        result[match[1]] = match[2]
      }

      // Also try to match integer values
      const intRegex = /<key>([^<]+)<\/key>\s*<integer>([^<]+)<\/integer>/g
      while ((match = intRegex.exec(content)) !== null) {
        result[match[1]] = match[2]
      }

      return Object.keys(result).length > 0 ? result : null
    } catch {
      return null
    }
  }
}
