import 'dart:io';

import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';

part 'mediafiles.dao.g.dart';

@DriftAccessor(tables: [MediaFiles, Messages])
class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
    with _$MediaFilesDaoMixin {
  // this constructor is required so that the main database can create an instance
  // of this object.
  // ignore: matching_super_parameters
  MediaFilesDao(super.db);

  Future<MediaFile?> insertOrUpdateMedia(MediaFilesCompanion mediaFile) async {
    try {
      var insertMediaFile = mediaFile;

      if (insertMediaFile.mediaId == const Value.absent()) {
        insertMediaFile = mediaFile.copyWith(
          mediaId: Value(uuid.v7()),
        );
      }

      await into(mediaFiles).insertOnConflictUpdate(insertMediaFile);

      final mediaId = insertMediaFile.mediaId.value;

      return await (select(
        mediaFiles,
      )..where((t) => t.mediaId.equals(mediaId))).getSingle();
    } catch (e) {
      Log.error('Could not insert media file: $e');
      return null;
    }
  }

  Future<void> deleteMediaFile(String mediaId) async {
    await (delete(mediaFiles)..where(
          (t) => t.mediaId.equals(mediaId),
        ))
        .go();
  }

  Future<void> updateMedia(
    String mediaId,
    MediaFilesCompanion updates,
  ) async {
    await (update(
      mediaFiles,
    )..where((c) => c.mediaId.equals(mediaId))).write(updates);
  }

  Future<void> updateAllMediaFiles(
    MediaFilesCompanion updates,
  ) async {
    await update(mediaFiles).write(updates);
  }

  Future<MediaFile?> getMediaFileById(String mediaId) async {
    return (select(
      mediaFiles,
    )..where((t) => t.mediaId.equals(mediaId))).getSingleOrNull();
  }

  Future<List<MediaFile>> getMediaFilesByIds(List<String> mediaIds) async {
    return (select(mediaFiles)..where((t) => t.mediaId.isIn(mediaIds))).get();
  }

  Future<MediaFile?> getDraftMediaFile() async {
    final medias = await (select(
      mediaFiles,
    )..where((t) => t.isDraftMedia.equals(true))).get();
    if (medias.isEmpty) {
      return null;
    }
    return medias.first;
  }

  Stream<MediaFile?> watchMedia(String mediaId) {
    return (select(
      mediaFiles,
    )..where((t) => t.mediaId.equals(mediaId))).watchSingleOrNull();
  }

  Future<void> resetPendingDownloadState() async {
    await (update(mediaFiles)..where(
          (c) => c.downloadState.equals(
            DownloadState.downloading.name,
          ),
        ))
        .write(
          const MediaFilesCompanion(
            downloadState: Value(DownloadState.pending),
          ),
        );
  }

  Future<List<MediaFile>> getAllMediaFilesPendingDownload() async {
    return (select(mediaFiles)..where(
          (t) =>
              t.downloadState.equals(DownloadState.pending.name) |
              t.downloadState.equals(DownloadState.downloading.name),
        ))
        .get();
  }

  Future<List<MediaFile>> getAllMediaFilesReuploadRequested() async {
    return (select(mediaFiles)..where(
          (t) => t.downloadState.equals(DownloadState.reuploadRequested.name),
        ))
        .get();
  }

  Future<List<MediaFile>> getAllMediaFilesPendingMigration() async {
    return (select(mediaFiles)..where(
          (t) =>
              t.stored.equals(true) &
              (t.storedFileHash.isNull() |
                  t.blurhash.isNull() |
                  t.hasCropAnalyzed.equals(false) |
                  (t.hasThumbnail.equals(false) &
                      t.type.equals(MediaType.audio.name).not()) |
                  t.sizeInBytes.isNull()),
        ))
        .get();
  }

  Future<List<MediaFile>> getAllMediaFilesPendingUpload() async {
    return (select(mediaFiles)..where(
          (t) =>
              (t.uploadState.equals(UploadState.initialized.name) |
              t.uploadState.equals(UploadState.uploadLimitReached.name) |
              t.uploadState.equals(UploadState.uploading.name) |
              t.uploadState.equals(UploadState.preprocessing.name)),
        ))
        .get();
  }

  Stream<List<MediaFile>> watchAllStoredMediaFiles() {
    final query =
        (select(mediaFiles)..where(
              (t) =>
                  t.stored.equals(true) |
                  t.cloudState.equals(CloudState.uploaded.name),
            ))
            .join([])
          ..groupBy([
            const CustomExpression<Object>(
              'COALESCE(stored_file_hash, media_id)',
            ),
          ]);
    return query.map((row) => row.readTable(mediaFiles)).watch();
  }

  Stream<List<MediaFile>> watchNewestMediaFiles() {
    return (select(mediaFiles)
          ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
          ..limit(100))
        .watch();
  }

  Stream<List<MediaFile>> watchMediaFilesForGroup(String groupId) {
    final query = select(mediaFiles).join([
      innerJoin(
        db.messages,
        db.messages.mediaId.equalsExp(mediaFiles.mediaId),
        useColumns: false,
      ),
    ])..where(db.messages.groupId.equals(groupId));
    return query.map((row) => row.readTable(mediaFiles)).watch();
  }

  Future<void> updateAllRetransmissionUploadingState() async {
    await (update(mediaFiles)..where(
          (t) =>
              t.uploadState.equals(UploadState.uploading.name) &
              t.reuploadRequestedBy.isNotNull(),
        ))
        .write(
          const MediaFilesCompanion(
            uploadState: Value(UploadState.preprocessing),
          ),
        );
  }

  Future<List<String>> getMessageIdsByMediaHash(
    Uint8List hash,
    int senderId,
  ) async {
    final query =
        select(db.messages).join([
          innerJoin(
            mediaFiles,
            mediaFiles.mediaId.equalsExp(db.messages.mediaId),
          ),
        ])..where(
          mediaFiles.storedFileHash.equals(hash) &
              db.messages.senderId.equals(senderId) &
              db.messages.openedAt.isNull(),
        );

    final rows = await query.get();
    return rows.map((row) => row.readTable(db.messages).messageId).toList();
  }

  Future<List<MediaFile>> getMediaByHash(Uint8List hash) async {
    final query = select(db.mediaFiles)
      ..where((t) => t.storedFileHash.equals(hash));
    return query.get();
  }

  Future<Map<MediaType, int>> getStorageStats() async {
    final rows = await select(mediaFiles).get();
    final stats = <MediaType, int>{};

    final Set<String> existingPaths;
    if (rows.isNotEmpty) {
      final dummyMs = MediaFileService(rows.first);
      final storedDir = dummyMs.storedPath.parent;
      if (storedDir.existsSync()) {
        existingPaths = storedDir
            .listSync()
            .whereType<File>()
            .map((f) => f.path)
            .toSet();
      } else {
        existingPaths = <String>{};
      }
    } else {
      existingPaths = <String>{};
    }

    for (final row in rows) {
      final type = row.type;
      final ms = MediaFileService(row);
      if (existingPaths.contains(ms.storedPath.path)) {
        final size = row.sizeInBytes ?? 0;
        stats[type] = (stats[type] ?? 0) + size;
      }
    }

    return stats;
  }

  Future<List<MediaFile>> getMemoriesToBackup() async {
    return (select(mediaFiles)..where(
          (t) =>
              t.stored.equals(true) & t.cloudState.equals(CloudState.none.name),
        ))
        .get();
  }

  Future<int> getCloudOnlyMemoriesCount() async {
    final query = select(mediaFiles)
      ..where(
        (t) =>
            t.stored.equals(true) &
            t.cloudState.equals(CloudState.uploaded.name),
      );
    final rows = await query.get();

    var count = 0;
    for (final row in rows) {
      final ms = MediaFileService(row);
      if (!(ms.storedPath.existsSync() && ms.storedPath.lengthSync() > 0)) {
        count++;
      }
    }
    return count;
  }
}
