import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { execSync } from 'child_process';

// CommonJS packages: use require so the constructor is available in Node
const AppInfoParser = require('app-info-parser') as new (file: string) => { parse: () => Promise<Record<string, unknown>> };
const plist = require('plist') as { parse: (xmlOrBuffer: string | Buffer) => Record<string, unknown> };
const AdmZip = require('adm-zip') as new (path?: string | Buffer) => {
  getEntry: (name: string) => { getData: () => Buffer } | null;
  getEntries: () => { isDirectory: boolean; entryName: string; getData: () => Buffer }[];
};

export interface AndroidMetadata {
  platform: 'android';
  appName: string;
  packageName: string;
  versionName: string;
  versionCode: string;
  minSdkVersion: string;
  /** Base64-encoded PNG icon if extracted */
  icon?: string | null;
  /** Build type: Debug or Release */
  buildType?: string;
}

export interface IosMetadata {
  platform: 'ios';
  appName: string;
  bundleIdentifier: string;
  version: string;
  buildNumber: string;
  minOsVersion: string;
  /** Base64-encoded PNG icon if extracted */
  icon?: string | null;
  /** Device UDIDs from the provisioning profile (ad-hoc/enterprise) */
  udids?: string[];
  /** Provisioning profile type: Development, AdHoc, Enterprise, AppStore */
  provisioningProfile?: string;
}

export type BuildMetadata = AndroidMetadata | IosMetadata;

@Injectable()
export class MetadataExtractionService {
  async extract(
    filePath: string,
    platform: 'android' | 'ios',
  ): Promise<BuildMetadata> {
    if (platform === 'android') {
      return this.extractAndroid(filePath);
    }
    return this.extractIos(filePath);
  }

  /** Get base64 icon string from parser result (data URI, string, Buffer, path to file, or { data/base64 }). */
  private normalizeIcon(icon: unknown): string | undefined {
    if (icon == null) return undefined;
    if (Buffer.isBuffer(icon)) return icon.toString('base64');
    if (typeof icon === 'string' && icon.length > 0) {
      // Handle data URIs like "data:image/png;base64,iVBORw0KGgo..."
      const dataUriMatch = icon.match(/^data:[^;]+;base64,(.+)$/s);
      if (dataUriMatch) {
        return dataUriMatch[1];
      }
      // Handle file paths
      if (icon.startsWith('/') || (icon.includes(path.sep) && !icon.startsWith('data:')) || icon.includes('\\')) {
        try {
          if (fs.existsSync(icon)) {
            const buf = fs.readFileSync(icon);
            return buf.toString('base64');
          }
        } catch {
          // ignore
        }
        return undefined;
      }
      return icon;
    }
    if (typeof icon === 'object' && icon !== null) {
      const o = icon as Record<string, unknown>;
      const s = (o.data ?? o.base64 ?? o.icon) as string | undefined;
      if (typeof s === 'string' && s.length > 0) return this.normalizeIcon(s);
      if (Buffer.isBuffer(o.data)) return (o.data as Buffer).toString('base64');
    }
    return undefined;
  }

  detectPlatform(originalName: string, mimeType?: string): 'android' | 'ios' | null {
    const lower = originalName.toLowerCase();
    if (lower.endsWith('.apk')) return 'android';
    if (lower.endsWith('.ipa')) return 'ios';
    if (mimeType === 'application/vnd.android.package-archive') return 'android';
    if (mimeType === 'application/octet-stream' && lower.endsWith('.ipa')) return 'ios';
    return null;
  }

  private async extractAndroid(filePath: string): Promise<AndroidMetadata> {
    try {
      const parser = new AppInfoParser(filePath);
      const result = (await parser.parse()) as Record<string, unknown>;
      const app = result.application as Record<string, unknown> | undefined;
      const label = app?.label;
      const labelStr = Array.isArray(label) ? label[0] : label;

      // Try to extract the largest icon from the APK's resource entries
      let icon = this.extractLargestApkIcon(filePath, app);
      if (!icon) {
        icon = this.normalizeIcon(result.icon);
      }
      
      const buildType = this.detectAndroidBuildType(filePath);
      
      return {
        platform: 'android',
        appName: (labelStr as string) || (result.package as string) || 'Unknown',
        packageName: String(result.package ?? ''),
        versionName: String(result.versionName ?? '0.0.0'),
        versionCode: String(result.versionCode ?? '0'),
        minSdkVersion: String(
          (result.usesSdk as Record<string, unknown>)?.['@_android:minSdkVersion'] ??
            (result.minSdkVersion as string) ??
            '0',
        ),
        icon: icon || undefined,
        buildType: buildType || undefined,
      };
    } catch (err) {
      const msg = err instanceof Error ? err.message : 'Parse failed';
      throw new Error(`Could not read APK metadata: ${msg}. Ensure the file is a valid Android APK.`);
    }
  }

  /** Extract the largest PNG icon from APK by reading the resource entries listed in application.icon. */
  private extractLargestApkIcon(filePath: string, app: Record<string, unknown> | undefined): string | undefined {
    try {
      const raw = app?.icon;
      const iconPaths = Array.isArray(raw) ? raw : typeof raw === 'string' && raw ? [raw] : [];
      if (iconPaths.length === 0) return undefined;
      const zip = new AdmZip(filePath);
      let largestBuf: Buffer | null = null;
      for (const iconPath of iconPaths) {
        if (typeof iconPath !== 'string') continue;
        const entry = zip.getEntry(iconPath);
        if (!entry) continue;
        const data = entry.getData();
        if (data.length > 0 && data.slice(0, 4).toString('hex') === '89504e47') {
          if (!largestBuf || data.length > largestBuf.length) {
            largestBuf = data;
          }
        }
      }
      return largestBuf ? largestBuf.toString('base64') : undefined;
    } catch {
      return undefined;
    }
  }

  private async extractIos(filePath: string): Promise<IosMetadata> {
    const parser = new AppInfoParser(filePath);
    const result = await parser.parse();
    const raw = result as Record<string, unknown>;
    const cfBundleShortVersion = raw.CFBundleShortVersionString ?? raw.cfbundleShortVersionString;
    const cfBundleVersion = raw.CFBundleVersion ?? raw.cfbundleVersion;
    const minOs = raw.MinimumOSVersion ?? raw.minimumOSVersion;
    const icon = this.normalizeIcon(raw.icon);
    const provisioningData = await this.extractProvisioningDataFromIpa(filePath);
    return {
      platform: 'ios',
      appName: (raw.CFBundleDisplayName ?? raw.CFBundleName ?? raw.cfbundleName ?? raw.CFBundleIdentifier) as string || 'Unknown',
      bundleIdentifier: (raw.CFBundleIdentifier ?? raw.cfbundleIdentifier) as string || '',
      version: (cfBundleShortVersion as string) || '0.0.0',
      buildNumber: (cfBundleVersion as string) || '0',
      minOsVersion: (minOs as string) || '0',
      icon: icon || undefined,
      udids: provisioningData.udids.length > 0 ? provisioningData.udids : undefined,
      provisioningProfile: provisioningData.profileType || undefined,
    };
  }

  /** Extract ProvisionedDevices UDIDs from embedded.mobileprovision inside the IPA. */
  private async extractUdidsFromIpa(filePath: string): Promise<string[]> {
    const data = await this.extractProvisioningDataFromIpa(filePath);
    return data.udids;
  }

  /** Extract provisioning profile data (UDIDs and profile type) from IPA. */
  private async extractProvisioningDataFromIpa(filePath: string): Promise<{ udids: string[]; profileType: string | null }> {
    const zip = new AdmZip(filePath);
    const entries = zip.getEntries();
    const provisionEntry = entries.find(
      (e) => !e.isDirectory && e.entryName.includes('.app/') && e.entryName.endsWith('embedded.mobileprovision'),
    );
    if (!provisionEntry) return { udids: [], profileType: null };
    const buf = provisionEntry.getData();
    if (!buf || buf.length === 0) return { udids: [], profileType: null };
    const tmpDir = os.tmpdir();
    const tmpIn = path.join(tmpDir, `provision-${Date.now()}-${Math.random().toString(36).slice(2)}.mobileprovision`);
    const tmpOut = path.join(tmpDir, `provision-out-${Date.now()}-${Math.random().toString(36).slice(2)}.plist`);
    try {
      fs.writeFileSync(tmpIn, buf);
      try {
        execSync(`openssl smime -inform der -verify -noverify -in "${tmpIn}" -out "${tmpOut}"`, {
          stdio: 'pipe',
          maxBuffer: 2 * 1024 * 1024,
        });
      } catch {
        return { udids: [], profileType: null };
      }
      if (!fs.existsSync(tmpOut)) return { udids: [], profileType: null };
      const xml = fs.readFileSync(tmpOut, 'utf8');
      const parsed = plist.parse(xml) as Record<string, unknown>;
      
      // Extract UDIDs
      const devices = parsed.ProvisionedDevices;
      const udids = Array.isArray(devices) ? devices.filter((d): d is string => typeof d === 'string') : [];
      
      // Determine profile type
      let profileType: string | null = null;
      
      // Check for get-task-allow in Entitlements
      const entitlements = parsed.Entitlements as Record<string, unknown> | undefined;
      const getTaskAllow = entitlements?.['get-task-allow'] === true || 
                          entitlements?.['get-task-allow'] === 'true' ||
                          parsed['get-task-allow'] === true || 
                          parsed['get-task-allow'] === 'true';
      
      const provisionedDevices = Array.isArray(parsed.ProvisionedDevices) && parsed.ProvisionedDevices.length > 0;
      const provisionAllDevices = parsed.ProvisionsAllDevices === true || parsed.ProvisionsAllDevices === 'true';
      
      if (provisionAllDevices) {
        profileType = 'Enterprise';
      } else if (getTaskAllow && provisionedDevices) {
        profileType = 'Development';
      } else if (!getTaskAllow && provisionedDevices) {
        profileType = 'AdHoc';
      } else if (!provisionedDevices && !provisionAllDevices) {
        profileType = 'AppStore';
      }
      
      return { udids, profileType };
    } finally {
      try {
        fs.unlinkSync(tmpIn);
      } catch {
        // ignore
      }
      try {
        fs.unlinkSync(tmpOut);
      } catch {
        // ignore
      }
    }
  }

  /** Detect Android build type by checking for debug signatures. */
  private detectAndroidBuildType(filePath: string): string | null {
    try {
      const zip = new AdmZip(filePath);
      const entries = zip.getEntries();
      
      // Check for debug keystore signatures
      const hasDebugRsa = entries.some(e => e.entryName === 'META-INF/CERT.RSA' || e.entryName === 'META-INF/ANDROIDDEBUGKEY.RSA');
      const hasDebugDsa = entries.some(e => e.entryName === 'META-INF/CERT.DSA' || e.entryName === 'META-INF/ANDROIDDEBUGKEY.DSA');
      
      // Read certificate to check for debug signature
      const certEntry = entries.find(e => e.entryName === 'META-INF/CERT.RSA' || e.entryName === 'META-INF/CERT.DSA');
      if (certEntry) {
        try {
          const certData = certEntry.getData();
          const certStr = certData.toString('utf8');
          // Debug builds typically have "Android Debug" or "androiddebugkey" in the certificate
          if (certStr.includes('androiddebugkey') || certStr.includes('Android Debug')) {
            return 'Debug';
          }
        } catch {
          // Ignore certificate read errors
        }
      }
      
      if (hasDebugRsa || hasDebugDsa) {
        return 'Debug';
      }
      
      // If we have certificates but no debug indicators, it's likely a release build
      const hasCertificates = entries.some(e => e.entryName.startsWith('META-INF/') && (e.entryName.endsWith('.RSA') || e.entryName.endsWith('.DSA')));
      if (hasCertificates) {
        return 'Release';
      }
      
      return null;
    } catch {
      return null;
    }
  }
}
