import 'dart:convert';

import 'package:drift/drift.dart';

import '../database/app_database.dart';

typedef JellyfinItemCacheKey = ({String scopeId, String machineId, String userId, String itemId});
typedef JellyfinCacheItem = ({ApiCacheData cacheRow, JellyfinItemCacheKey key});
typedef ResolvedJellyfinCacheItem = ({ApiCacheData cacheRow, ConnectionRow connection, JellyfinItemCacheKey key});

/// Canonical Jellyfin connection and item-cache key resolution.
class JellyfinCacheResolver {
  JellyfinCacheResolver(this.database);

  final AppDatabase database;

  static const _likeEscape = r'\';
  static const _usersMarker = ':/Users/';
  static const _itemsMarker = '/Items/';

  Expression<bool> itemKeyPredicate(GeneratedColumn<String> column, String serverOrScopeId, String itemId) {
    final scope = _splitScope(serverOrScopeId);
    final escapedItemId = _escapeLike(itemId);
    final scopedUser = scope.userId == null ? '%' : _escapeLike(scope.userId!);
    final scopedPattern = '${_escapeLike(serverOrScopeId)}:/Users/$scopedUser/Items/$escapedItemId';
    var predicate = column.like(scopedPattern, escapeChar: _likeEscape);

    if (scope.userId == null) {
      final compoundPattern = '${_escapeLike(scope.machineId)}/%:/Users/%/Items/$escapedItemId';
      predicate = predicate | column.like(compoundPattern, escapeChar: _likeEscape);
    } else {
      final legacyPattern = '${_escapeLike(scope.machineId)}:/Users/${_escapeLike(scope.userId!)}/Items/$escapedItemId';
      predicate = predicate | column.like(legacyPattern, escapeChar: _likeEscape);
    }
    return predicate;
  }

  Future<JellyfinCacheItem?> findItem(String serverOrScopeId, String itemId) async {
    final matches = await _findItems(serverOrScopeId, itemId);
    return matches.isEmpty ? null : matches.first;
  }

  Future<ResolvedJellyfinCacheItem?> findResolvedItem(String serverOrScopeId, String itemId) async {
    final matches = await _findItems(serverOrScopeId, itemId);
    for (final match in matches) {
      final connection = await findConnection(match.key.scopeId, userId: match.key.userId);
      if (connection != null) return (cacheRow: match.cacheRow, connection: connection, key: match.key);
    }
    return null;
  }

  Future<List<JellyfinCacheItem>> _findItems(String serverOrScopeId, String itemId) async {
    final rows =
        await (database.select(database.apiCache)
              ..where((t) => itemKeyPredicate(t.cacheKey, serverOrScopeId, itemId))
              ..orderBy([(t) => OrderingTerm.asc(t.cacheKey)]))
            .get();
    final requested = _splitScope(serverOrScopeId);
    final matches = <JellyfinCacheItem>[];
    for (final row in rows) {
      final key = parseItemKey(row.cacheKey);
      if (key == null || key.itemId != itemId) continue;
      if (key.machineId != requested.machineId) continue;
      if (requested.userId != null && key.userId != requested.userId) continue;
      matches.add((cacheRow: row, key: key));
    }
    if (requested.userId != null) {
      matches.sort((a, b) => a.key.scopeId == serverOrScopeId ? -1 : (b.key.scopeId == serverOrScopeId ? 1 : 0));
    }
    return matches;
  }

  Future<List<ResolvedJellyfinCacheItem>> findPinnedItems() async {
    final rows =
        await (database.select(database.apiCache)
              ..where((t) => t.pinned.equals(true))
              ..orderBy([(t) => OrderingTerm.asc(t.cacheKey)]))
            .get();
    if (rows.isEmpty) return const [];

    final connections = await (database.select(database.connections)..where((t) => t.kind.equals('jellyfin'))).get();
    final connectionById = {for (final connection in connections) connection.id: connection};
    final bindings = await database.select(database.profileConnections).get();
    final bindingsByConnection = <String, List<ProfileConnectionRow>>{};
    for (final binding in bindings) {
      bindingsByConnection.putIfAbsent(binding.connectionId, () => []).add(binding);
    }

    bool matchesBinding(String connectionId, String userId) {
      final connectionBindings = bindingsByConnection[connectionId];
      return connectionBindings == null ||
          connectionBindings.isEmpty ||
          connectionBindings.any((binding) => binding.userIdentifier == userId);
    }

    final matches = <ResolvedJellyfinCacheItem>[];
    for (final row in rows) {
      final key = parseItemKey(row.cacheKey);
      if (key == null) continue;
      final compoundId = '${key.machineId}/${key.userId}';
      final compound = connectionById[compoundId];
      if (compound != null && matchesBinding(compound.id, key.userId)) {
        matches.add((cacheRow: row, connection: compound, key: key));
        continue;
      }
      final legacy = connectionById[key.machineId];
      if (legacy != null && matchesBinding(legacy.id, key.userId)) {
        matches.add((cacheRow: row, connection: legacy, key: key));
      }
    }
    return matches;
  }

  /// Resolves the exact persisted Jellyfin cache namespace owned by
  /// [profileId] for [serverOrScopeId].
  ///
  /// The physical download row is deliberately not consulted: it is shared
  /// across profiles and may have been created by a different Jellyfin user.
  Future<String?> findProfileScopeId(String serverOrScopeId, String profileId) async {
    if (profileId.isEmpty) return null;
    final requested = _splitScope(serverOrScopeId);
    final bindings =
        await (database.select(database.profileConnections)
              ..where((t) => t.profileId.equals(profileId))
              ..orderBy([
                (t) => OrderingTerm.desc(t.isDefault),
                (t) => OrderingTerm.desc(t.lastUsedAt),
                (t) => OrderingTerm.asc(t.connectionId),
              ]))
            .get();
    for (final binding in bindings) {
      if (binding.userIdentifier.isEmpty) continue;
      final connection = await (database.select(
        database.connections,
      )..where((t) => t.id.equals(binding.connectionId) & t.kind.equals('jellyfin'))).getSingleOrNull();
      if (connection == null) continue;

      final connectionScope = _splitScope(connection.id);
      var machineId = connectionScope.machineId;
      String? configuredUserId = connectionScope.userId;
      try {
        final config = jsonDecode(connection.configJson);
        if (config is Map<String, dynamic>) {
          final configuredMachineId = config['serverMachineId'];
          final configuredUser = config['userId'];
          if (configuredMachineId is String && configuredMachineId.isNotEmpty) {
            machineId = configuredMachineId;
          }
          if (configuredUser is String && configuredUser.isNotEmpty) {
            configuredUserId = configuredUser;
          }
        }
      } on FormatException {
        // Legacy rows can still be resolved from their canonical id.
      }
      if (machineId != requested.machineId) continue;
      if (configuredUserId != null && configuredUserId != binding.userIdentifier) continue;
      return '$machineId/${binding.userIdentifier}';
    }
    return null;
  }

  Future<ConnectionRow?> findConnection(String serverOrScopeId, {String? userId}) async {
    final scope = _splitScope(serverOrScopeId);
    if (scope.userId != null && userId != null && scope.userId != userId) return null;
    final expectedUserId = userId ?? scope.userId;

    if (expectedUserId != null) {
      final compoundId = '${scope.machineId}/$expectedUserId';
      final compound = await (database.select(
        database.connections,
      )..where((t) => t.id.equals(compoundId) & t.kind.equals('jellyfin'))).getSingleOrNull();
      if (compound != null && await _matchesProfileBinding(compound.id, expectedUserId)) return compound;

      final legacy = await (database.select(
        database.connections,
      )..where((t) => t.id.equals(scope.machineId) & t.kind.equals('jellyfin'))).getSingleOrNull();
      if (legacy != null && await _matchesProfileBinding(legacy.id, expectedUserId)) return legacy;
      return null;
    }

    final exact = await (database.select(
      database.connections,
    )..where((t) => t.id.equals(scope.machineId))).getSingleOrNull();
    if (exact != null) return exact;

    final plex = await _findPlexConnectionForServer(scope.machineId);
    if (plex != null) return plex;

    final prefix = '${scope.machineId}/';
    return (database.select(database.connections)
          ..where((t) => t.id.substr(1, prefix.length).equals(prefix) & t.kind.equals('jellyfin'))
          ..orderBy([(t) => OrderingTerm.asc(t.id)])
          ..limit(1))
        .getSingleOrNull();
  }

  Future<ConnectionRow?> _findPlexConnectionForServer(String serverId) async {
    final accounts = await (database.select(database.connections)..where((t) => t.kind.equals('plex'))).get();
    for (final account in accounts) {
      try {
        final config = jsonDecode(account.configJson);
        if (config is! Map<String, dynamic>) continue;
        final servers = config['servers'];
        if (servers is! List) continue;
        for (final server in servers) {
          if (server is Map && server['clientIdentifier'] == serverId) {
            return account;
          }
        }
      } on FormatException {
        // Ignore malformed persisted accounts and continue deterministically.
      }
    }
    return null;
  }

  Future<bool> _matchesProfileBinding(String connectionId, String userId) async {
    final bindings = await (database.select(
      database.profileConnections,
    )..where((t) => t.connectionId.equals(connectionId))).get();
    return bindings.isEmpty || bindings.any((binding) => binding.userIdentifier == userId);
  }

  static JellyfinItemCacheKey? parseItemKey(String cacheKey) {
    final usersMarker = cacheKey.indexOf(_usersMarker);
    if (usersMarker <= 0) return null;
    final scopeId = cacheKey.substring(0, usersMarker);
    final userStart = usersMarker + _usersMarker.length;
    final itemsMarker = cacheKey.indexOf(_itemsMarker, userStart);
    if (itemsMarker <= userStart) return null;
    final userId = cacheKey.substring(userStart, itemsMarker);
    final itemId = cacheKey.substring(itemsMarker + _itemsMarker.length);
    if (itemId.isEmpty || itemId.contains('/') || itemId.contains('?')) return null;

    final scope = _splitScope(scopeId);
    if (scope.userId != null && scope.userId != userId) return null;
    return (scopeId: scopeId, machineId: scope.machineId, userId: userId, itemId: itemId);
  }

  static ({String machineId, String? userId}) _splitScope(String scopeId) {
    final slash = scopeId.indexOf('/');
    if (slash <= 0 || slash == scopeId.length - 1) return (machineId: scopeId, userId: null);
    return (machineId: scopeId.substring(0, slash), userId: scopeId.substring(slash + 1));
  }

  static String _escapeLike(String value) {
    return value.replaceAll(_likeEscape, '$_likeEscape$_likeEscape').replaceAll('%', r'\%').replaceAll('_', r'\_');
  }
}
