import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import * as bcrypt from 'bcrypt';
import { App } from '../../entities/app.entity';
import { Build } from '../../entities/build.entity';
import { User } from '../../entities/user.entity';
import { GoogleDriveService } from '../drive/google-drive.service';
import { MetadataExtractionService, IosMetadata } from './services/metadata-extraction.service';

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

  constructor(
    @InjectRepository(App)
    private readonly appRepo: Repository<App>,
    @InjectRepository(Build)
    private readonly buildRepo: Repository<Build>,
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
    private readonly driveService: GoogleDriveService,
    private readonly metadataService: MetadataExtractionService,
  ) { }

  async findOrCreateApp(
    userId: string,
    platform: 'android' | 'ios',
    bundleOrPackage: string,
    appName: string,
  ): Promise<App> {
    let app = await this.appRepo.findOne({
      where: { userId, platform, bundleOrPackage },
    });
    if (app) {
      if (appName && app.appName !== appName) {
        app.appName = appName;
        await this.appRepo.save(app);
      }
      return app;
    }
    app = this.appRepo.create({
      userId,
      platform,
      bundleOrPackage,
      appName: appName || bundleOrPackage,
    });
    return this.appRepo.save(app);
  }

  async createBuild(
    user: User,
    filePath: string,
    originalName: string,
    mimeType: string,
    releaseNotes?: string,
    password?: string,
  ): Promise<Build> {
    const platform = this.metadataService.detectPlatform(originalName, mimeType);
    if (!platform) {
      throw new Error('Unsupported file. Use .apk (Android) or .ipa (iOS).');
    }
    const metadata = await this.metadataService.extract(filePath, platform);
    const bundleOrPackage =
      metadata.platform === 'android'
        ? metadata.packageName
        : metadata.bundleIdentifier;
    const version =
      metadata.platform === 'android' ? metadata.versionName : metadata.version;
    const buildNumber =
      metadata.platform === 'android' ? metadata.versionCode : metadata.buildNumber;
    const minOs =
      metadata.platform === 'android'
        ? metadata.minSdkVersion
        : metadata.minOsVersion;
    const appName =
      metadata.platform === 'android' ? metadata.appName : metadata.appName;

    const app = await this.findOrCreateApp(
      user.id,
      platform,
      bundleOrPackage,
      appName,
    );

    await this.validateVersionAndBuild(app.id, version, buildNumber);

    const fs = await import('fs');
    const stat = fs.statSync(filePath);
    const size = Number(stat.size) || 0;
    const buildId = uuidv4();

    const driveFileId = await this.driveService.uploadBuild(
      user,
      appName,
      platform,
      bundleOrPackage,
      version,
      buildNumber,
      filePath,
      mimeType,
      originalName,
    );

    let plistFileId: string | null = null;
    if (platform === 'ios') {
      const isLocal = process.env.NODE_ENV !== 'production' && (!process.env.FRONTEND_URL || process.env.FRONTEND_URL.includes('localhost'));
      const defaultApiUrl = isLocal ? 'http://localhost:4000' : 'https://deployhub-back.workzy.co';
      let apiUrl = (process.env.PUBLIC_BASE_URL || defaultApiUrl).trim();
      if (apiUrl.endsWith('/')) apiUrl = apiUrl.slice(0, -1);
      if (apiUrl.includes('deployhub.workzy.co') && !apiUrl.includes('deployhub-back.workzy.co')) {
        apiUrl = 'https://deployhub-back.workzy.co';
      }
      const ipaUrl = `${apiUrl}/api/install/ipa/${buildId}`;
      const plistContent = this.buildIosManifestPlist(
        metadata.platform === 'ios' ? metadata : null,
        ipaUrl,
        appName,
      );
      plistFileId = await this.driveService.uploadPlist(
        user,
        appName,
        platform,
        bundleOrPackage,
        version,
        buildNumber,
        plistContent,
      );
    }

    // Save app icon: store base64 in DB for reliable serving, also upload to Drive as backup
    const iconB64 = metadata.icon;
    if (iconB64 && typeof iconB64 === 'string') {
      try {
        const iconBuffer = Buffer.from(iconB64, 'base64');
        if (iconBuffer.length > 0 && iconBuffer.length < 5 * 1024 * 1024) {
          app.iconData = iconB64;

          // Also upload to Drive as backup (non-blocking)
          try {
            const iconFileId = await this.driveService.uploadIcon(
              user,
              appName,
              platform,
              iconBuffer,
            );
            this.logger.log(`Icon uploaded to Drive: ${iconFileId}`);
            app.iconFileId = iconFileId;
          } catch (driveErr: any) {
            this.logger.warn(`Drive icon upload failed (DB icon saved): ${driveErr.message}`);
          }

          await this.appRepo.save(app);
          this.logger.log(`Icon saved to DB for app ${app.id}`);
        } else {
          this.logger.warn(`Icon buffer size invalid: ${iconBuffer.length}`);
        }
      } catch (err: any) {
        this.logger.error(`Icon save failed: ${err.message}`, err.stack);
      }
    } else if (!app.iconData && !app.iconFileId) {
      this.logger.warn(
        `No icon in metadata (type=${typeof iconB64}); app icon may be missing`,
      );
    }

    const shortCode = await this.generateUniqueShortCode();
    const udids = platform === 'ios' && metadata.platform === 'ios' && metadata.udids?.length
      ? metadata.udids
      : undefined;
    const provisioningProfile = platform === 'ios' && metadata.platform === 'ios'
      ? metadata.provisioningProfile
      : undefined;
    const buildType = metadata.platform === 'android'
      ? metadata.buildType
      : (metadata.platform === 'ios' ? metadata.provisioningProfile : undefined);

    // Hash password if provided
    let passwordHash: string | null = null;
    const isPasswordProtected = !!password?.trim();
    if (isPasswordProtected && password) {
      passwordHash = await bcrypt.hash(password, 10);
    }

    const build = this.buildRepo.create({
      id: buildId,
      appId: app.id,
      version,
      buildNumber,
      minOs: minOs || null,
      driveFileId,
      plistFileId,
      size,
      shortCode,
      updateDescription: await this.processReleaseNotes(releaseNotes),
      udids: udids ?? null,
      provisioningProfile: provisioningProfile ?? null,
      buildType: buildType ?? null,
      passwordHash,
      passwordText: isPasswordProtected ? (password?.trim() || null) : null,
      isPasswordProtected,
    });
    await this.buildRepo.save(build);
    return build;
  }

  /**
   * Compares two semver-like version strings (e.g. "1.2.3", "2.0").
   * Returns  1 if a > b, -1 if a < b, 0 if equal.
   */
  private compareVersions(a: string, b: string): number {
    const partsA = a.split('.').map((s) => parseInt(s, 10) || 0);
    const partsB = b.split('.').map((s) => parseInt(s, 10) || 0);
    const len = Math.max(partsA.length, partsB.length);
    for (let i = 0; i < len; i++) {
      const numA = partsA[i] ?? 0;
      const numB = partsB[i] ?? 0;
      if (numA > numB) return 1;
      if (numA < numB) return -1;
    }
    return 0;
  }

  /**
   * Compares two build numbers. Handles purely numeric (Android versionCode)
   * and dotted strings (iOS CFBundleVersion like "3.2.1").
   */
  private compareBuildNumbers(a: string, b: string): number {
    const numA = Number(a);
    const numB = Number(b);
    if (!isNaN(numA) && !isNaN(numB)) {
      return numA > numB ? 1 : numA < numB ? -1 : 0;
    }
    return this.compareVersions(a, b);
  }

  /**
   * Validates that the new version/build is not a downgrade or duplicate.
   *
   * Rules:
   *  - Cannot upload a version lower than the highest existing version.
   *  - If same version, build number must be strictly higher than the highest existing build.
   *  - Exact duplicate (same version + same build) is also rejected.
   */
  private async validateVersionAndBuild(
    appId: string,
    version: string,
    buildNumber: string,
  ): Promise<void> {
    const existingBuilds = await this.buildRepo.find({
      where: { appId },
      select: ['id', 'version', 'buildNumber', 'shortCode'],
    });

    if (existingBuilds.length === 0) return;

    let highestVersion = existingBuilds[0].version;
    for (const b of existingBuilds) {
      if (this.compareVersions(b.version, highestVersion) > 0) {
        highestVersion = b.version;
      }
    }

    const versionCmp = this.compareVersions(version, highestVersion);

    if (versionCmp < 0) {
      const error: any = new Error(
        `Version ${version} is lower than the latest version ${highestVersion}. Please upload version ${highestVersion} or higher.`,
      );
      error.code = 'VERSION_DOWNGRADE';
      throw error;
    }

    if (versionCmp === 0) {
      const sameVersionBuilds = existingBuilds.filter(
        (b) => this.compareVersions(b.version, version) === 0,
      );

      let highestBuild = sameVersionBuilds[0].buildNumber;
      let highestBuildEntry = sameVersionBuilds[0];
      for (const b of sameVersionBuilds) {
        if (this.compareBuildNumbers(b.buildNumber, highestBuild) > 0) {
          highestBuild = b.buildNumber;
          highestBuildEntry = b;
        }
      }

      const buildCmp = this.compareBuildNumbers(buildNumber, highestBuild);

      if (buildCmp === 0) {
        const error: any = new Error(
          `Version ${version} (Build ${buildNumber}) is already uploaded.`,
        );
        error.code = 'DUPLICATE_BUILD';
        error.buildId = highestBuildEntry.id;
        error.shortCode = highestBuildEntry.shortCode;
        throw error;
      }

      if (buildCmp < 0) {
        const error: any = new Error(
          `Build ${buildNumber} is lower than the latest build ${highestBuild} for version ${version}. Please upload build number higher than ${highestBuild}.`,
        );
        error.code = 'BUILD_DOWNGRADE';
        throw error;
      }
    }
  }

  private static readonly SHORT_CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

  private async generateUniqueShortCode(): Promise<string> {
    for (let attempt = 0; attempt < 20; attempt++) {
      let code = '';
      for (let i = 0; i < 8; i++) {
        code += BuildService.SHORT_CODE_CHARS[
          Math.floor(Math.random() * BuildService.SHORT_CODE_CHARS.length)
        ];
      }
      const existing = await this.buildRepo.findOne({ where: { shortCode: code } });
      if (!existing) return code;
    }
    throw new Error('Could not generate unique short code');
  }

  async getBuildIdByShortCode(shortCode: string): Promise<string | null> {
    const build = await this.buildRepo.findOne({
      where: { shortCode: shortCode.toUpperCase() },
      select: ['id'],
    });
    return build?.id ?? null;
  }

  async getBuildById(buildId: string): Promise<Build | null> {
    return this.buildRepo.findOne({
      where: { id: buildId },
      relations: ['app'],
    });
  }

  async getBuildsByUser(userId: string): Promise<Build[]> {
    return this.buildRepo.find({
      where: { app: { userId } },
      relations: ['app'],
      order: { createdAt: 'DESC' },
    });
  }

  async getAppsByUser(userId: string): Promise<App[]> {
    return this.appRepo.find({
      where: { userId },
      relations: ['builds'],
      order: { createdAt: 'DESC' },
    });
  }

  async updateBuildReleaseNotes(
    buildId: string,
    userId: string,
    updateDescription: string,
  ): Promise<Build | null> {
    const build = await this.buildRepo.findOne({
      where: { id: buildId },
      relations: ['app'],
    });
    if (!build || build.app.userId !== userId) return null;
    build.updateDescription = updateDescription || null;
    await this.buildRepo.save(build);
    return build;
  }

  async updateBuild(
    buildId: string,
    userId: string,
    updateDescription?: string,
    password?: string,
    removePassword?: boolean,
  ): Promise<Build | null> {
    const build = await this.buildRepo.findOne({
      where: { id: buildId },
      relations: ['app'],
    });
    if (!build || build.app.userId !== userId) return null;

    // Update release notes if provided
    if (typeof updateDescription === 'string') {
      build.updateDescription = updateDescription || null;
    }

    // Handle password updates
    if (removePassword === true) {
      build.passwordHash = null;
      build.passwordText = null;
      build.isPasswordProtected = false;
    } else if (password && password.trim()) {
      const hash = await bcrypt.hash(password.trim(), 10);
      build.passwordHash = hash;
      build.passwordText = password.trim();
      build.isPasswordProtected = true;
    }

    await this.buildRepo.save(build);
    return build;
  }

  async deleteBuild(buildId: string, userId: string): Promise<boolean> {
    const build = await this.buildRepo.findOne({
      where: { id: buildId },
      relations: ['app'],
    });
    if (!build || build.app.userId !== userId) return false;
    const user = await this.userRepo.findOne({ where: { id: userId } });
    if (user) {
      try {
        await this.driveService.deleteFile(build.driveFileId, user);
        if (build.plistFileId) await this.driveService.deleteFile(build.plistFileId, user);
      } catch {
        // Continue with DB removal even if Drive delete fails
      }
    }
    await this.buildRepo.remove(build);
    return true;
  }

  async verifyBuildPassword(buildId: string, password: string): Promise<boolean> {
    const build = await this.buildRepo.findOne({
      where: { id: buildId },
      select: ['id', 'passwordHash', 'isPasswordProtected'],
    });
    if (!build) return false;
    if (!build.isPasswordProtected) return true;
    if (!build.passwordHash) return false;
    return bcrypt.compare(password, build.passwordHash);
  }

  async deleteApp(appId: string, userId: string): Promise<boolean> {
    const app = await this.appRepo.findOne({
      where: { id: appId, userId },
      relations: ['builds'],
    });
    if (!app) return false;
    const user = await this.userRepo.findOne({ where: { id: userId } });
    if (user) {
      await this.driveService.deleteAppPlatformFolder(user, app.appName, app.platform).catch(() => { });
    }
    await this.appRepo.remove(app);
    return true;
  }

  private buildIosManifestPlist(
    metadata: IosMetadata | null,
    ipaUrl: string,
    appName: string,
  ): string {
    const bundleId = metadata?.bundleIdentifier ?? 'unknown';
    const version = metadata?.version ?? '1.0';
    const title = appName || 'App';
    return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>items</key>
  <array>
    <dict>
      <key>assets</key>
      <array>
        <dict>
          <key>kind</key>
          <string>software-package</string>
          <key>url</key>
          <string>${ipaUrl.replace(/&/g, '&amp;')}</string>
        </dict>
      </array>
      <key>metadata</key>
      <dict>
        <key>bundle-identifier</key>
        <string>${bundleId}</string>
        <key>bundle-version</key>
        <string>${version}</string>
        <key>kind</key>
        <string>software</string>
        <key>title</key>
        <string>${this.escapeXml(title)}</string>
      </dict>
    </dict>
  </array>
</dict>
</plist>`;
  }

  private async processReleaseNotes(notes?: string): Promise<string | null> {
    const trimmed = notes?.trim();
    if (!trimmed) return null;
    if (!this.looksLikeCommitMessages(trimmed)) return trimmed;
    return this.improveReleaseNotesWithAI(trimmed);
  }

  private looksLikeCommitMessages(text: string): boolean {
    const lines = text.split('\n').filter((l) => l.trim());
    if (lines.length === 0) return false;

    const commitPrefixes = /^-?\s*(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)[:(]/i;
    const bulletPrefix = /^- /;

    let matchCount = 0;
    for (const line of lines) {
      if (commitPrefixes.test(line.trim()) || bulletPrefix.test(line.trim())) {
        matchCount++;
      }
    }
    return matchCount >= lines.length * 0.5;
  }

  private static readonly GROQ_API_URL = 'https://api.groq.com/openai/v1/chat/completions';
  private static readonly GROQ_MODEL = 'llama-3.1-8b-instant';
  private static readonly CI_RELEASE_NOTES_PROMPT = `You convert git commit messages into professional mobile app release notes.

Rules:
- Summarize related commits into grouped points (don't repeat similar items)
- Start with "What's new in this build:" followed by bullets using "- " prefix
- Each bullet should be 1 clear sentence in past tense (e.g. "Implemented", "Fixed", "Updated")
- Ignore merge commits, version bumps, and CI-related commits (like "chore: update deps", "ci: fix workflow")
- If only 1 meaningful change, return a single sentence with no intro line and no bullet
- Return ONLY the release notes text, no markdown formatting or extra commentary
- Maximum 8 bullet points; combine minor items if needed`;

  private async improveReleaseNotesWithAI(rawNotes: string): Promise<string> {
    const apiKey = process.env.GROQ_API_KEY;
    if (!apiKey) {
      this.logger.warn('GROQ_API_KEY not set; using raw commit messages as release notes');
      return rawNotes;
    }

    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 10_000);

      const res = await fetch(BuildService.GROQ_API_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${apiKey}`,
        },
        body: JSON.stringify({
          model: BuildService.GROQ_MODEL,
          messages: [
            { role: 'system', content: BuildService.CI_RELEASE_NOTES_PROMPT },
            { role: 'user', content: rawNotes },
          ],
          temperature: 0.15,
          max_tokens: 500,
          top_p: 0.8,
        }),
        signal: controller.signal,
      });

      clearTimeout(timeout);

      if (!res.ok) {
        this.logger.warn(`Groq API returned ${res.status}; falling back to raw commit messages`);
        return rawNotes;
      }

      const data = await res.json();
      const improved: string | undefined = data?.choices?.[0]?.message?.content;

      if (!improved?.trim()) {
        this.logger.warn('Groq returned empty response; using raw commit messages');
        return rawNotes;
      }

      this.logger.log('AI-improved release notes generated from commit messages');
      return improved.trim();
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      this.logger.warn(`AI release notes generation failed (${message}); using raw commit messages`);
      return rawNotes;
    }
  }

  private escapeXml(s: string): string {
    return s
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }
}
