import {
  BadGatewayException,
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
  ServiceUnavailableException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { readFile } from 'fs/promises';
import { GoogleAuth } from 'google-auth-library';
import { buildAiMusicShareUrl, resolveShareBaseUrl } from '../../common/utils/share-url.util';
import { generateWaveformPeakSetFromBuffer } from '../../common/utils/waveform.util';
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
import { MediaProbeService } from '../../infrastructure/storage/media-probe.service';
import { PostsService } from '../posts/posts.service';
import { AiMusicPromptEnhancerService } from './ai-music-prompt-enhancer.service';
import { AiMusicArchiveQueryDto } from './dto/ai-music-archive-query.dto';
import { ShareAiMusicArchiveToFeedDto } from './dto/share-ai-music-archive.dto';
import { TextToMusicDto } from './dto/text-to-music.dto';
import { UpdateAiMusicArchiveDto } from './dto/update-ai-music-archive.dto';
import { MediaRepository } from './media.repository';
import {
  AiMusicArchiveDocument,
  AiMusicArchiveType,
} from './schemas/ai-music-archive.schema';

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

  constructor(
    private readonly configService: ConfigService,
    private readonly storageService: ManagedStorageService,
    private readonly mediaProbeService: MediaProbeService,
    private readonly promptEnhancer: AiMusicPromptEnhancerService,
    private readonly mediaRepository: MediaRepository,
    private readonly postsService: PostsService,
  ) {}

  async getMediaHealth() {
    const [storageHealth, ffmpeg, ffprobe] = await Promise.all([
      this.storageService.getHealth(),
      this.mediaProbeService.checkFfmpeg(),
      this.mediaProbeService.checkFfprobe(),
    ]);
    const storageProvider =
      this.configService.get<string>('storage.provider', { infer: true }) ?? 'local';
    const publicBaseUrl = this.configService.get<string>('publicBaseUrl', { infer: true }) ?? '';
    const warnings: string[] = [];
    const imageProcessingEnabled =
      this.configService.get<boolean>('imageProcessing.enabled', { infer: true }) ?? false;
    const videoProcessingEnabled =
      this.configService.get<boolean>('videoProcessing.enabled', { infer: true }) ?? false;
    const videoHlsGenerationEnabled =
      this.configService.get<boolean>('videoProcessing.generateHls', { infer: true }) ?? true;
    const videoThumbnailGenerationEnabled =
      this.configService.get<boolean>('videoProcessing.generateThumbnails', { infer: true }) ??
      true;
    const audioProcessingEnabled =
      this.configService.get<boolean>('audioProcessing.enabled', { infer: true }) ?? false;

    if (storageProvider === 'local') {
      warnings.push(
        'Local storage requires persistent volume mounted to /app/uploads in production',
      );
    }
    if (!publicBaseUrl) {
      warnings.push('PUBLIC_BASE_URL is not configured');
    }
    if ((imageProcessingEnabled || videoProcessingEnabled) && !ffmpeg.available) {
      warnings.push('ffmpeg is not available; image/video processing may fail');
    }
    if (!ffprobe.available) {
      warnings.push('ffprobe is not available; duration extraction may fail');
    }
    if (storageProvider === 's3' && !(storageHealth.isS3Configured ?? storageHealth.s3Configured)) {
      warnings.push('S3 provider selected but missing required env variables');
    }
    if (storageProvider === 's3' && !storageHealth.storagePublicBaseUrlConfigured) {
      warnings.push(
        'STORAGE_PUBLIC_BASE_URL is not configured; media may be served from the storage endpoint instead of CDN',
      );
    }
    const s3Health = storageHealth.s3 as Record<string, unknown> | undefined;
    if (storageProvider === 's3' && s3Health?.reachable === false) {
      warnings.push('S3 bucket is not reachable from the backend');
    }

    const storageWritable =
      storageProvider !== 'local' || storageHealth.uploadPathWritable !== false;
    const status = !storageWritable ? 'error' : warnings.length ? 'warning' : 'ok';

    return {
      status,
      storage: storageHealth,
      processing: {
        imageProcessingEnabled,
        videoProcessingEnabled,
        videoHlsGenerationEnabled,
        videoThumbnailGenerationEnabled,
        audioProcessingEnabled,
        ffmpegPath: ffmpeg.path,
        ffmpegAvailable: ffmpeg.available,
        ffmpegVersion: ffmpeg.version,
        ffprobePath: ffprobe.path,
        ffprobeAvailable: ffprobe.available,
        ffprobeVersion: ffprobe.version,
        ffmpeg,
        ffprobe,
      },
      serving: {
        rangeRequests: true,
        immutableCacheSeconds: 31536000,
        hlsManifestCacheSeconds: 300,
        s3ImmutableCacheControl: 'public, max-age=31536000, immutable',
        s3HlsManifestCacheControl: 'public, max-age=300',
      },
      staticServing: {
        uploadsPublicPath:
          storageHealth.uploadsPublicPath ?? storageHealth.publicPath ?? '/uploads',
        rangeRequestsExpected: true,
        cacheHeadersExpected: true,
        hlsMimeExpected: 'application/vnd.apple.mpegurl',
      },
      warnings,
    };
  }

  async generateMusicFromText(userId: string, dto: TextToMusicDto) {
    const enabled = this.configService.get<boolean>('aiMusic.enabled', { infer: true });
    if (!enabled) {
      throw new ServiceUnavailableException('AI music generation is disabled');
    }

    const apiKey = this.configService.get<string>('aiMusic.apiKey', { infer: true }) ?? '';
    const projectId = this.configService.get<string>('aiMusic.projectId', { infer: true }) ?? '';
    const location = this.configService.get<string>('aiMusic.location', { infer: true }) ?? '';
    const model = this.configService.get<string>('aiMusic.model', { infer: true }) ?? 'lyria-002';
    if (!projectId || !location) {
      throw new ServiceUnavailableException('AI music settings are not configured');
    }

    let url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:predict`;
    let authorizationHeader: string | null = null;

    if (apiKey) {
      url = `${url}?key=${encodeURIComponent(apiKey)}`;
    } else {
      const auth = new GoogleAuth({
        scopes: ['https://www.googleapis.com/auth/cloud-platform'],
        ...this.resolveGoogleApplicationCredentials(),
      });

      let accessToken = '';
      try {
        const client = await auth.getClient();
        const accessTokenRaw = await client.getAccessToken();
        accessToken =
          typeof accessTokenRaw === 'string' ? accessTokenRaw : (accessTokenRaw?.token ?? '');
      } catch (error) {
        throw new ServiceUnavailableException(
          `Failed to authenticate with Google Cloud: ${
            error instanceof Error ? error.message : 'invalid credentials'
          }`,
        );
      }

      if (!accessToken) {
        throw new ServiceUnavailableException(
          'Failed to authenticate with Google Cloud: access token was not returned',
        );
      }
      authorizationHeader = `Bearer ${accessToken}`;
    }

    const promptEnhancement = this.promptEnhancer.enhance(dto);
    const requestBody: Record<string, unknown> = {
      instances: [{ prompt: promptEnhancement.enhancedPrompt }],
      parameters: {
        sampleCount: 1,
        durationSeconds: dto.durationSeconds ?? 12,
      },
    };

    if (typeof dto.seed === 'number') {
      (requestBody.parameters as Record<string, unknown>).seed = dto.seed;
    }

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        ...(authorizationHeader ? { Authorization: authorizationHeader } : {}),
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(requestBody),
    });

    const result = (await response.json()) as {
      error?: { message?: string };
      predictions?: Array<{
        bytesBase64Encoded?: string;
        mimeType?: string;
        audio?: { bytesBase64Encoded?: string; mimeType?: string };
      }>;
    };

    if (!response.ok) {
      throw new BadGatewayException(
        result?.error?.message ?? 'Google AI returned an unexpected error',
      );
    }

    const first = result?.predictions?.[0];
    const audioBase64 = first?.bytesBase64Encoded ?? first?.audio?.bytesBase64Encoded ?? '';
    const mimeType = first?.mimeType ?? first?.audio?.mimeType ?? 'audio/wav';

    if (!audioBase64) {
      throw new BadGatewayException('Google AI did not return audio content');
    }

    const extension = this.resolveAudioExtension(mimeType);
    const buffer = Buffer.from(audioBase64, 'base64');
    const audioUrl = await this.storageService.saveFile({
      folderSegments: ['ai-music'],
      extension: `.${extension}`,
      buffer,
      contentType: mimeType,
      fileNamePrefix: `ai-${userId}`,
    });
    const savedAudioPath = this.storageService.resolveLocalFilePath(audioUrl);
    const [actualDurationSeconds, waveformBuffer] = await Promise.all([
      this.resolveSavedAudioDurationSeconds(savedAudioPath, buffer, extension, mimeType),
      this.resolveSavedAudioBuffer(savedAudioPath, buffer),
    ]);

    const waveformSet = generateWaveformPeakSetFromBuffer(
      waveformBuffer,
      actualDurationSeconds ?? dto.durationSeconds ?? 12,
    );

    const generationResult = {
      prompt: dto.prompt,
      originalPrompt: promptEnhancement.originalPrompt,
      enhancedPrompt: promptEnhancement.enhancedPrompt,
      detectedMusicIntent: promptEnhancement.detectedMusicIntent,
      durationSeconds: actualDurationSeconds ?? dto.durationSeconds ?? 12,
      mimeType,
      sizeBytes: buffer.length,
      audioUrl,
      ...waveformSet,
    };

    try {
      const archiveItem = await this.mediaRepository.createAiMusicArchiveItem(userId, {
        type: AiMusicArchiveType.TEXT_TO_MUSIC,
        title: this.buildArchiveTitle(dto.prompt),
        prompt: dto.prompt,
        tag: 'text-to-music',
        audioUrl,
        storageKey: audioUrl,
        durationSeconds: generationResult.durationSeconds,
        mimeType,
        sizeBytes: buffer.length,
        waveformPeaks: generationResult.waveformPeaks,
      });

      return {
        ...generationResult,
        archiveItemId: archiveItem.id,
        archiveItem: this.formatAiMusicArchiveItem(archiveItem),
      };
    } catch (error) {
      this.logger.warn(
        `AI music archive save failed for user=${userId}: ${
          error instanceof Error ? error.message : 'unknown error'
        }`,
      );
      return generationResult;
    }
  }

  async getMyAiMusicArchive(userId: string, query: AiMusicArchiveQueryDto) {
    const page = query.page ?? 1;
    const limit = Math.min(query.limit ?? 20, 50);
    const skip = (page - 1) * limit;
    const [items, total] = await Promise.all([
      this.mediaRepository.findAiMusicArchiveItemsByUser(userId, skip, limit),
      this.mediaRepository.countAiMusicArchiveItemsByUser(userId),
    ]);

    return {
      items: items.map((item) => this.formatAiMusicArchiveItem(item)),
      pagination: {
        page,
        limit,
        hasMore: skip + items.length < total,
      },
    };
  }

  async getAiMusicArchiveItem(userId: string, itemId: string) {
    const item = await this.findOwnedAiMusicArchiveItemOrFail(userId, itemId);
    return this.formatAiMusicArchiveItem(item);
  }

  async renameAiMusicArchiveItem(userId: string, itemId: string, dto: UpdateAiMusicArchiveDto) {
    const title = dto.title.trim();
    if (!title) {
      throw new BadRequestException('title must not be empty');
    }

    const item = await this.mediaRepository.updateAiMusicArchiveTitle(userId, itemId, title);
    if (!item) {
      throw new NotFoundException('Archive item not found');
    }

    return this.formatAiMusicArchiveItem(item);
  }

  async deleteAiMusicArchiveItem(userId: string, itemId: string): Promise<{ message: string }> {
    const deleted = await this.mediaRepository.softDeleteAiMusicArchiveItem(userId, itemId);
    if (!deleted) {
      throw new NotFoundException('Archive item not found');
    }

    return { message: 'Archive item deleted' };
  }

  async shareAiMusicArchiveItem(userId: string, itemId: string) {
    const item = await this.findOwnedAiMusicArchiveItemOrFail(userId, itemId);
    const formatted = this.formatAiMusicArchiveItem(item);

    return {
      shareUrl: buildAiMusicShareUrl(this.resolveShareBaseUrl(), item.id),
      title: formatted.title,
      audioUrl: formatted.audioUrl,
    };
  }

  async shareAiMusicArchiveItemToFeed(
    userId: string,
    itemId: string,
    dto: ShareAiMusicArchiveToFeedDto = {},
  ) {
    const item = await this.findOwnedAiMusicArchiveItemOrFail(userId, itemId);
    const formatted = this.formatAiMusicArchiveItem(item);

    return this.postsService.create(userId, {
      content: dto.content?.trim() || formatted.title,
      audioUrl: item.audioUrl,
      durationSeconds: item.durationSeconds ?? undefined,
      waveformPeaks: item.waveformPeaks,
      style: item.tag,
    });
  }

  private async resolveSavedAudioDurationSeconds(
    savedAudioPath: string | null,
    buffer: Buffer,
    extension: string,
    mimeType: string,
  ): Promise<number | null> {
    if (savedAudioPath) {
      const durationSeconds = await this.mediaProbeService.extractDurationSeconds(savedAudioPath);
      if (durationSeconds) {
        return durationSeconds;
      }
    }

    return this.mediaProbeService.extractDurationSecondsFromBuffer(buffer, {
      originalname: `ai-music.${extension}`,
      mimetype: mimeType,
    });
  }

  private async resolveSavedAudioBuffer(
    savedAudioPath: string | null,
    fallbackBuffer: Buffer,
  ): Promise<Buffer> {
    if (!savedAudioPath) {
      return fallbackBuffer;
    }

    try {
      return await readFile(savedAudioPath);
    } catch {
      return fallbackBuffer;
    }
  }

  private resolveAudioExtension(mimeType: string): string {
    if (mimeType.includes('mpeg') || mimeType.includes('mp3')) {
      return 'mp3';
    }
    if (mimeType.includes('ogg')) {
      return 'ogg';
    }
    if (mimeType.includes('aac')) {
      return 'aac';
    }
    if (mimeType.includes('wav')) {
      return 'wav';
    }
    return 'wav';
  }

  private async findOwnedAiMusicArchiveItemOrFail(
    userId: string,
    itemId: string,
  ): Promise<AiMusicArchiveDocument> {
    const item = await this.mediaRepository.findAiMusicArchiveItemByUser(userId, itemId);
    if (!item) {
      throw new NotFoundException('Archive item not found');
    }

    return item;
  }

  private formatAiMusicArchiveItem(item: AiMusicArchiveDocument) {
    const value = item.toObject() as Record<string, any>;
    return {
      id: value.id ?? value._id?.toString?.() ?? item.id,
      type: value.type,
      title: value.title,
      prompt: value.prompt,
      tag: value.tag,
      audioUrl: value.audioUrl,
      storageKey: value.storageKey ?? '',
      durationSeconds: value.durationSeconds ?? null,
      mimeType: value.mimeType ?? '',
      sizeBytes: value.sizeBytes ?? 0,
      waveformPeaks: Array.isArray(value.waveformPeaks) ? value.waveformPeaks : [],
      createdAt: value.createdAt,
      updatedAt: value.updatedAt,
    };
  }

  private buildArchiveTitle(prompt: string): string {
    const compactPrompt = prompt.replace(/\s+/g, ' ').trim();
    if (!compactPrompt) {
      return 'Untitled AI Music';
    }

    return compactPrompt.length > 80 ? `${compactPrompt.slice(0, 77)}...` : compactPrompt;
  }

  private resolveShareBaseUrl(): string {
    return resolveShareBaseUrl(this.configService);
  }

  private resolveGoogleApplicationCredentials(): {
    credentials?: any;
  } {
    const encodedCredentials = (
      this.configService.get<string>('aiMusic.googleApplicationCredentialsJsonBase64', {
        infer: true,
      }) ?? ''
    ).trim();
    if (!encodedCredentials) {
      return {};
    }

    try {
      const decoded = Buffer.from(encodedCredentials, 'base64').toString('utf8');
      const credentials = JSON.parse(decoded) as Record<string, unknown>;
      if (!credentials || typeof credentials !== 'object') {
        throw new Error('decoded credentials are not a JSON object');
      }
      if (typeof credentials.client_email !== 'string' || !credentials.client_email) {
        throw new Error('missing client_email');
      }
      if (typeof credentials.private_key !== 'string' || !credentials.private_key) {
        throw new Error('missing private_key');
      }

      return { credentials };
    } catch (error) {
      throw new ServiceUnavailableException(
        `Invalid GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64: ${
          error instanceof Error ? error.message : 'unable to decode credentials'
        }`,
      );
    }
  }
}
