import { Injectable } from '@nestjs/common';
import { Readable } from 'stream';
import { google } from 'googleapis';
import { User } from '../../entities/user.entity';
import { GoogleAuthService } from '../auth/google-auth.service';

const DEPLOYHUB_FOLDER = 'DeployHub';

/** Sanitize for Drive folder name: no path chars or reserved chars */
function sanitizeFolderName(name: string): string {
  return name
    .replace(/[/\\:*?"<>|]/g, '-')
    .replace(/\s+/g, ' ')
    .trim() || 'App';
}

@Injectable()
export class GoogleDriveService {
  constructor(private googleAuth: GoogleAuthService) { }

  private async getDriveClient(user: User, retryCount = 0): Promise<ReturnType<typeof google.drive>> {
    try {
      const accessToken = await this.googleAuth.getValidAccessToken(user);
      const auth = new google.auth.OAuth2();
      auth.setCredentials({ access_token: accessToken });
      return google.drive({ version: 'v3', auth });
    } catch (error: any) {
      // If token refresh fails and we haven't retried, try once more
      if (retryCount === 0 && error.message.includes('Token refresh failed')) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return this.getDriveClient(user, retryCount + 1);
      }
      throw error;
    }
  }

  async ensureFolderPath(
    drive: Awaited<ReturnType<typeof google.drive>>,
    pathParts: string[],
    parentId?: string,
  ): Promise<string> {
    let parent = parentId ?? 'root';
    for (const name of pathParts) {
      const res = await drive.files.list({
        q: `'${parent}' in parents and name = '${name.replace(/'/g, "\\'")}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false`,
        fields: 'files(id)',
        spaces: 'drive',
      });
      let folderId: string;
      if (res.data.files?.length) {
        folderId = res.data.files[0].id!;
      } else {
        const create = await drive.files.create({
          requestBody: {
            name,
            mimeType: 'application/vnd.google-apps.folder',
            parents: [parent],
          },
          fields: 'id',
        });
        folderId = create.data.id!;
      }
      parent = folderId;
    }
    return parent;
  }

  /** Platform folder label: "iOS" or "Android" */
  private platformFolderName(platform: string): string {
    return platform.toLowerCase() === 'ios' ? 'iOS' : 'Android';
  }

  async uploadBuild(
    user: User,
    appName: string,
    platform: string,
    bundleOrPackage: string,
    version: string,
    buildNumber: string,
    filePath: string,
    mimeType: string,
    fileName: string,
  ): Promise<string> {
    const drive = await this.getDriveClient(user);
    const path = await import('path');
    const fs = await import('fs');
    const ext = path.extname(fileName) || '';

    const pathParts = [
      DEPLOYHUB_FOLDER,
      sanitizeFolderName(appName),
      this.platformFolderName(platform),
    ];

    if (platform.toLowerCase() === 'ios') {
      const safeVersion = sanitizeFolderName(`${version}(${buildNumber})`);
      pathParts.push(safeVersion);
    }

    const folderId = await this.ensureFolderPath(drive, pathParts);

    // Use "AppName version.apk" or "AppName version.ipa" format for file naming
    const uploadName = `${sanitizeFolderName(appName)} ${version}${ext}`;

    const res = await drive.files.create({
      requestBody: {
        name: uploadName,
        parents: [folderId],
      },
      media: {
        mimeType,
        body: fs.createReadStream(filePath),
      },
      fields: 'id',
    });
    const fileId = res.data.id;
    if (!fileId) throw new Error('Drive upload failed');
    await drive.permissions.create({
      fileId,
      requestBody: {
        role: 'reader',
        type: 'anyone',
      },
    });
    return fileId;
  }

  /** Upload app icon (PNG buffer) to app/platform folder (one icon per app per platform).
   * If an icon already exists, it will be replaced with the new one. */
  async uploadIcon(
    user: User,
    appName: string,
    platform: string,
    iconBuffer: Buffer,
  ): Promise<string> {
    const drive = await this.getDriveClient(user);
    const pathParts = [
      DEPLOYHUB_FOLDER,
      sanitizeFolderName(appName),
      this.platformFolderName(platform),
    ];
    const folderId = await this.ensureFolderPath(drive, pathParts);

    // Check if icon.png already exists in this folder
    const existingFiles = await drive.files.list({
      q: `'${folderId}' in parents and name = 'icon.png' and trashed = false`,
      fields: 'files(id, name)',
      spaces: 'drive',
    });

    // If icon exists, delete it first
    if (existingFiles.data.files && existingFiles.data.files.length > 0) {
      for (const file of existingFiles.data.files) {
        try {
          await drive.files.delete({ fileId: file.id! });
        } catch (err) {
          // Continue even if delete fails
        }
      }
    }

    // Upload the new icon
    const res = await drive.files.create({
      requestBody: {
        name: 'icon.png',
        parents: [folderId],
        mimeType: 'image/png',
      },
      media: {
        mimeType: 'image/png',
        body: Readable.from(iconBuffer),
      },
      fields: 'id',
    });
    const fileId = res.data.id;
    if (!fileId) throw new Error('Drive icon upload failed');
    await drive.permissions.create({
      fileId,
      requestBody: { role: 'reader', type: 'anyone' },
    });
    return fileId;
  }

  async uploadPlist(
    user: User,
    appName: string,
    platform: string,
    bundleOrPackage: string,
    version: string,
    buildNumber: string,
    plistContent: string,
  ): Promise<string> {
    const drive = await this.getDriveClient(user);
    const folderId = await this.ensureFolderPath(drive, [
      DEPLOYHUB_FOLDER,
      sanitizeFolderName(appName),
      this.platformFolderName(platform),
      sanitizeFolderName(`${version}(${buildNumber})`),
    ]);
    const res = await drive.files.create({
      requestBody: {
        name: 'manifest.plist',
        parents: [folderId],
        mimeType: 'application/xml',
      },
      media: {
        mimeType: 'application/xml',
        body: Readable.from(Buffer.from(plistContent, 'utf-8')),
      },
      fields: 'id',
    });
    const fileId = res.data.id;
    if (!fileId) throw new Error('Drive plist upload failed');
    await drive.permissions.create({
      fileId,
      requestBody: {
        role: 'reader',
        type: 'anyone',
      },
    });
    return fileId;
  }

  async getFileDownloadUrl(user: User, fileId: string): Promise<string> {
    const drive = await this.getDriveClient(user);
    const res = await drive.files.get({
      fileId,
      fields: 'id',
    });
    if (!res.data.id) throw new Error('File not found');
    return `https://drive.google.com/uc?export=download&id=${fileId}`;
  }

  getPublicFileUrl(fileId: string): string {
    return `https://drive.usercontent.google.com/download?id=${fileId}&export=download&confirm=t`;
  }

  /** Direct image URL for embedding in img tags — uses lh3 (no redirect, avoids CORS issues). */
  getThumbnailUrl(fileId: string, size: number = 256): string {
    return `https://lh3.googleusercontent.com/d/${fileId}=w${size}`;
  }

  /**
   * Check if the file is still available (not trashed/deleted from Drive).
   * Uses the Drive API with user credentials for a reliable check, including
   * detecting files whose parent folder was trashed/deleted.
   *
   * When the owner's OAuth credentials fail (expired/revoked), we assume the
   * file is still available rather than returning false — all DeployHub uploads
   * have `anyone:reader` permission, so the file remains publicly accessible
   * even when the owner's tokens are invalid.
   */
  async isFileAvailable(fileId: string, user?: User): Promise<boolean> {
    if (user) {
      try {
        const drive = await this.getDriveClient(user);
        const res = await drive.files.get({ fileId, fields: 'id,trashed,parents' });
        if (!res.data.id || res.data.trashed === true) return false;

        const parents = res.data.parents;
        if (parents && parents.length > 0) {
          try {
            const parentRes = await drive.files.get({
              fileId: parents[0],
              fields: 'id,trashed',
            });
            if (!parentRes.data.id || parentRes.data.trashed === true) return false;
          } catch {
            return false;
          }
        }

        return true;
      } catch (err: any) {
        const code = err?.code ?? err?.response?.status;
        if (code === 404) return false;
        // OAuth/credential errors (expired tokens, revoked access, etc.) should
        // NOT mark the file as unavailable — the file itself is still public.
        // Fall through to the public check below.
        console.warn(`Drive API check failed for file ${fileId} (will try public check):`, err instanceof Error ? err.message : err);
      }
    }

    // Public availability check using the newer Google Drive URL.
    // Only returns false when we get a definitive "file not found" from Google.
    // For any ambiguous result, we assume available and let the actual download
    // attempt handle it — better to show the install button and let the
    // redirect work than to falsely block a public build.
    try {
      const url = `https://drive.usercontent.google.com/download?id=${fileId}&export=download&confirm=t`;
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 8000);
      try {
        const res = await fetch(url, {
          method: 'HEAD',
          redirect: 'follow',
          signal: controller.signal,
          headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
        });

        if (res.status === 404) return false;
        // Any 2xx/3xx = file exists
        return res.status < 400;
      } finally {
        clearTimeout(timeout);
      }
    } catch (err) {
      console.warn(`Public check failed for file ${fileId}:`, err instanceof Error ? err.message : err);
    }

    // If all checks failed (network/timeout), assume available — the download
    // controller will redirect to Google Drive directly as the ultimate fallback.
    return true;
  }

  /** Stream file content from Drive using the owner's credentials. */
  async streamFile(
    user: User,
    fileId: string,
  ): Promise<{ stream: import('stream').Readable; mimeType: string; name: string; size?: number }> {
    const drive = await this.getDriveClient(user);
    const meta = await drive.files.get({ fileId, fields: 'name,mimeType,size' });
    const res = await drive.files.get(
      { fileId, alt: 'media' },
      { responseType: 'stream' },
    );
    return {
      stream: res.data as unknown as import('stream').Readable,
      mimeType: meta.data.mimeType ?? 'application/octet-stream',
      name: meta.data.name ?? 'file',
      size: meta.data.size ? Number(meta.data.size) : undefined,
    };
  }

  /**
   * Stream file via Google Drive's public download URL (no OAuth required).
   * Works for files that have `anyone:reader` permission (all DeployHub uploads do).
   * Used as a fallback when the owner's OAuth tokens are expired/revoked.
   *
   * Tries multiple URL formats in order of reliability.
   */
  async streamFilePublic(
    fileId: string,
  ): Promise<{ stream: import('stream').Readable; size?: number }> {
    const urls = [
      `https://drive.usercontent.google.com/download?id=${fileId}&export=download&confirm=t`,
      `https://drive.google.com/uc?export=download&id=${fileId}&confirm=t`,
    ];

    const errors: string[] = [];

    for (const url of urls) {
      try {
        const res = await fetch(url, {
          redirect: 'follow',
          headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
        });

        if (!res.ok || !res.body) {
          errors.push(`${url} → HTTP ${res.status}`);
          continue;
        }

        const contentType = res.headers.get('content-type') || '';
        if (contentType.includes('text/html')) {
          const html = await res.text();
          // Google "file not found" pages
          if (
            html.includes('Sorry, the file you have requested does not exist') ||
            html.includes('the item you have requested is not found')
          ) {
            throw new Error('File does not exist on Google Drive');
          }
          // Virus scan confirmation — extract token and retry
          const confirmMatch = html.match(/confirm=([0-9A-Za-z_-]+)/) ||
                              html.match(/export=download[^"]*?&amp;confirm=([^"&]+)/) ||
                              html.match(/id="download-form"[^>]*action="([^"]+)"/);
          if (confirmMatch) {
            const confirmUrl = confirmMatch[0].startsWith('http')
              ? confirmMatch[0]
              : `${url}&confirm=${confirmMatch[1]}`;
            const confirmRes = await fetch(confirmUrl, {
              redirect: 'follow',
              headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
            });
            if (confirmRes.ok && confirmRes.body) {
              const contentLength = confirmRes.headers.get('content-length');
              return {
                stream: Readable.fromWeb(confirmRes.body as any),
                size: contentLength ? Number(contentLength) : undefined,
              };
            }
          }
          errors.push(`${url} → returned HTML (not a downloadable file)`);
          continue;
        }

        const contentLength = res.headers.get('content-length');
        return {
          stream: Readable.fromWeb(res.body as any),
          size: contentLength ? Number(contentLength) : undefined,
        };
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (msg === 'File does not exist on Google Drive') throw err;
        errors.push(`${url} → ${msg}`);
      }
    }

    throw new Error(`All public download URLs failed: ${errors.join('; ')}`);
  }

  /** Get icon file as buffer for serving through API */
  async getIconBuffer(user: User, fileId: string): Promise<Buffer> {
    const drive = await this.getDriveClient(user);
    const res = await drive.files.get(
      { fileId, alt: 'media' },
      { responseType: 'arraybuffer' },
    );
    return Buffer.from(res.data as ArrayBuffer);
  }

  /** Delete a file from Drive (e.g. when user deletes build in DeployHub). */
  async deleteFile(fileId: string, user: User): Promise<void> {
    const drive = await this.getDriveClient(user);
    try {
      await drive.files.delete({ fileId });
    } catch (err: unknown) {
      const code = (err as { code?: number })?.code;
      if (code === 404) return;
      throw err;
    }
  }

  /** Delete the platform folder from Drive: DeployHub/{appName}/{platform} */
  async deleteAppPlatformFolder(user: User, appName: string, platform: string): Promise<void> {
    const drive = await this.getDriveClient(user);
    try {
      const safeName = sanitizeFolderName(appName);
      const platformName = this.platformFolderName(platform);

      const rootRes = await drive.files.list({
        q: `'root' in parents and name = '${DEPLOYHUB_FOLDER.replace(/'/g, "\\'")}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false`,
        fields: 'files(id)',
        spaces: 'drive',
      });
      const rootFolder = rootRes.data.files?.[0];
      if (!rootFolder?.id) return;

      const appRes = await drive.files.list({
        q: `'${rootFolder.id}' in parents and name = '${safeName.replace(/'/g, "\\'")}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false`,
        fields: 'files(id)',
        spaces: 'drive',
      });
      const appFolder = appRes.data.files?.[0];
      if (!appFolder?.id) return;

      const platformRes = await drive.files.list({
        q: `'${appFolder.id}' in parents and name = '${platformName.replace(/'/g, "\\'")}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false`,
        fields: 'files(id)',
        spaces: 'drive',
      });
      const platformFolder = platformRes.data.files?.[0];
      if (!platformFolder?.id) return;

      await drive.files.delete({ fileId: platformFolder.id });
    } catch {
      // Best-effort: don't fail the delete operation if folder cleanup fails
    }
  }
}
