import 'dart:async';
import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/libphylum/db/resource_helpers.dart';
import 'package:phylum/libphylum/local_upload_errors.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/util/dialogs.dart';
import 'package:phylum/util/file_size.dart';
import 'package:provider/provider.dart';

final slashChar = '/'.runes.first;
final illegalChars = <int>{
  ...'\\/:*?"<>|'.runes,
};

bool validateName(String name) =>
    !name.startsWith(' ') &&
    name != '.' &&
    name != '..' &&
    name.trim().isNotEmpty &&
    !name.runes.any((c) => c < 31 || illegalChars.contains(c));

Future<void> createDirectory(BuildContext context, Resource parent, {String? preset}) async {
  final account = context.read<PhylumAccount>();
  final name = await showInputDialog(
    context,
    title: 'Create New Folder',
    labelText: 'Folder Name',
    preset: preset,
    validate: validateName,
  );
  if (name == null || !context.mounted) return;
  await handleLocalErrors(
    context,
    name,
    overwriteFn: (r) => nameConflictDelete,
    (name, conflictResolution, ensurePermission) => account.resourceRepository.mkdir(
      parent: parent.id,
      name: name,
      userPermission: parent.userPermission,
      conflictResolution: conflictResolution,
      ensurePermission: ensurePermission,
    ),
  );
}

Future<void> pickAndUploadFiles(BuildContext context, String folderId) async {
  final files = await openFiles();
  if (!context.mounted) return;
  return uploadXFiles(context, folderId, files);
}

Future<void> uploadXFiles(BuildContext context, String folderId, Iterable<XFile> files) async {
  if (files.isEmpty) return;
  final account = context.read<PhylumAccount>();
  final size =
      await Future.wait(files.map((files) => files.length())).then((sizes) => sizes.fold(0, (acc, s) => acc + s));
  if (!context.mounted) return;
  final confirm = await showAlertDialog(
        context,
        title: 'Upload ${files.length} Files?',
        message: 'Total Upload Size: ${size.formatForDisplay()}',
        positiveText: 'YES',
        negativeText: 'NO',
      ) ??
      false;
  final userPermission = await account.db.getResource(folderId).then((r) => r?.userPermission) ?? 0;
  if (!confirm || !context.mounted) return;
  for (final file in files) {
    String? id;
    ensurefile:
    while (id == null) {
      id = await handleLocalErrors(
        // ignore: use_build_context_synchronously
        context,
        p.basename(file.name),
        overwriteFn: (r) => r.dir ? nameConflictDelete : nameConflictOverwrite,
        (name, conflictResolution, ensurePermission) => account.resourceRepository.upload(
          parent: folderId,
          name: name,
          file: file,
          userPermission: userPermission,
          conflictResolution: conflictResolution,
          ensurePermission: ensurePermission,
        ),
      );
      if (id == null && context.mounted) {
        final confirm =
            await showAlertDialog(context, title: 'Skip file?', positiveText: 'No', negativeText: 'Yes') ?? false;
        if (!confirm) break ensurefile;
      }
    }
  }
}

Future<void> pickAndUploadDirectory(BuildContext context, String folderId) async {
  final path = await getDirectoryPath();
  if (path == null || !context.mounted) return;
  return uploadPath(context, folderId, [path]);
}

Future<void> uploadPath(BuildContext context, String folderId, Iterable<String> paths) async {
  final account = context.read<PhylumAccount>();
  showProgressDialog(context, message: 'Counting Files');
  DirTree? tree;
  for (final path in paths) {
    final stat = await DirTree.stat(path);
    tree = tree?.mergeWith(stat) ?? stat;
  }

  if (!context.mounted) return;
  Navigator.of(context).pop();

  if (tree == null) return;
  final confirm = await showAlertDialog(
        context,
        title: 'Upload Files?',
        message:
            'Found ${tree.files.length} files and ${tree.dirs.length} directories.\n\nTotal Upload Size: ${tree.totalSize.formatForDisplay()}',
        positiveText: 'YES',
        negativeText: 'NO',
      ) ??
      false;
  final userPermission = await account.db.getResource(folderId).then((r) => r?.userPermission) ?? 0;
  if (!confirm || !context.mounted) return;
  final dirIds = <String, String>{};
  for (final path in tree.dirs) {
    final parent = dirIds[p.dirname(path)] ?? folderId;
    String? id;
    while (id == null) {
      id = await handleLocalErrors(
        // ignore: use_build_context_synchronously
        context,
        p.basename(path),
        overwriteFn: (r) => nameConflictDelete,
        (name, conflictResolution, ensurePermission) => account.resourceRepository.mkdir(
          parent: parent,
          name: name,
          userPermission: userPermission,
          conflictResolution: conflictResolution,
          ensurePermission: ensurePermission,
        ),
      );
      if (id == null) {
        if (!context.mounted) return;
        final confirm =
            await showAlertDialog(context, title: 'Cancel operation?', positiveText: 'No', negativeText: 'Yes') ??
                false;
        if (!confirm) return;
      }
    }
    dirIds[path] = id;
  }

  for (final path in tree.files) {
    final parent = dirIds[p.dirname(path)] ?? folderId;
    String? id;
    ensurefile:
    while (id == null) {
      id = await handleLocalErrors(
        // ignore: use_build_context_synchronously
        context,
        p.basename(path),
        overwriteFn: (r) => r.dir ? nameConflictDelete : nameConflictOverwrite,
        (name, conflictResolution, ensurePermission) => account.resourceRepository.upload(
          parent: parent,
          name: name,
          path: path,
          userPermission: userPermission,
          conflictResolution: conflictResolution,
          ensurePermission: ensurePermission,
        ),
      );
      if (id == null && context.mounted) {
        final confirm =
            await showAlertDialog(context, title: 'Skip file?', positiveText: 'No', negativeText: 'Yes') ?? false;
        if (!confirm) break ensurefile;
      }
    }
  }
}

Future<String?> handleLocalErrors(
  BuildContext context,
  String name,
  Future<String> Function(String, NameConflictResolution, bool) performAction, {
  NameConflictResolution? Function(Resource)? overwriteFn,
  NameConflictResolution conflictResolution = nameConflictError,
  bool ensurePermission = true,
}) async {
  try {
    return await performAction(name, conflictResolution, ensurePermission);
  } on PermissionException {
    if (!context.mounted) return null;
    final skip = await showAlertDialog(
          context,
          title: 'Permissions Error',
          message: 'You do not have permissions to write to this folder.',
          positiveText: 'OK',
          negativeText: 'Try Anyway',
        ) ??
        true;
    if (!skip && context.mounted) {
      return handleLocalErrors(
        context,
        name,
        performAction,
        overwriteFn: overwriteFn,
        conflictResolution: conflictResolution,
        ensurePermission: false,
      );
    }
  } on NameConflictException catch (e) {
    if (!context.mounted) return null;
    final overwrite = overwriteFn?.call(e.resource);
    final newResolution = await showDialog<NameConflictResolution>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Name Conflict'),
        content: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 360),
          child: Text(
            'There is already another item with the name \'$name\' in this folder.',
            softWrap: true,
          ),
        ),
        actions: <Widget>[
          if (overwrite != null)
            TextButton(
              style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error),
              child: const Text('Overwrite'),
              onPressed: () {
                Navigator.of(context).pop(overwrite);
              },
            ),
          TextButton(
            child: const Text('Change Name'),
            onPressed: () {
              Navigator.of(context).pop(nameConflictError);
            },
          ),
          ElevatedButton(
            autofocus: true,
            child: const Text('Auto Rename'),
            onPressed: () {
              Navigator.of(context).pop(nameConflictRename);
            },
          ),
        ],
      ),
    );
    if (newResolution == null) {
      return null;
    }
    if (newResolution == nameConflictError) {
      if (!context.mounted) return null;
      final newName = await showInputDialog(
        context,
        title: 'Change Name',
        labelText: 'New Name',
        preset: name,
        validate: validateName,
      );
      if (newName == null) {
        return null;
      }
      name = newName;
    }
    if (!context.mounted) return null;
    return handleLocalErrors(
      context,
      name,
      performAction,
      overwriteFn: overwriteFn,
      conflictResolution: newResolution,
      ensurePermission: ensurePermission,
    );
  } on DestinationCycleException catch (e) {
    if (context.mounted) {
      await showAlertDialog(context, message: 'Cannot move ${e.resource.name} into itself');
    }
  } catch (e) {
    if (context.mounted) {
      await showAlertDialog(context, title: 'Unable to create $name', message: e.toString());
    }
  }

  return null;
}

class DirTree {
  final int numFiles;
  final int numDirs;
  final int totalSize;
  final List<String> files;
  final List<String> dirs;

  DirTree({
    required this.numFiles,
    required this.numDirs,
    required this.totalSize,
    required this.files,
    required this.dirs,
  });

  DirTree mergeWith(DirTree other) {
    return DirTree(
      numFiles: numFiles + other.numFiles,
      numDirs: numDirs + other.numDirs,
      totalSize: totalSize + other.totalSize,
      files: List.of(files)..addAll(other.files),
      dirs: List.of(dirs)..addAll(other.dirs),
    );
  }

  static Future<DirTree> stat(String path) {
    final s = FileStat.statSync(path);
    if (s.type == FileSystemEntityType.file) {
      return Future.value(DirTree(
        numFiles: 1,
        numDirs: 0,
        totalSize: s.size,
        files: [path],
        dirs: const [],
      ));
    }
    final dir = Directory(path);
    final completer = Completer<DirTree>();
    final files = <String>[];
    final dirs = <String>[path];
    int numFiles = 0;
    int numDirs = 0;
    int totalSize = 0;

    dir.list(recursive: true).listen(
      (file) {
        if (file is File) {
          numFiles++;
          totalSize += file.lengthSync();
          files.add(file.path);
        }
        if (file is Directory) {
          numDirs++;
          dirs.add(file.path);
        }
      },
      onDone: () => completer.complete(DirTree(
        numFiles: numFiles,
        numDirs: numDirs,
        totalSize: totalSize,
        files: files,
        dirs: dirs,
      )),
    );
    return completer.future;
  }
}
