import 'dart:convert';
import 'dart:io';

import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;

import 'manifest.dart';

/// Pulls the remote season manifest and downloads new/updated packs into
/// [cacheDir] with SHA256 verification and atomic (temp + rename) writes.
/// All failures are swallowed into a `false` return — remote content is an
/// enhancement, never a crash source.
class ContentDownloader {
  ContentDownloader({
    required this.baseUrl,
    required this.cacheDir,
    http.Client? client,
  }) : _client = client ?? http.Client() {
    _loadIndex();
  }

  final String baseUrl;
  final Directory cacheDir;
  final http.Client _client;
  final Map<String, int> _index = {};

  File get _indexFile => File('${cacheDir.path}/cache_index.json');

  void _loadIndex() {
    try {
      if (_indexFile.existsSync()) {
        final json =
            jsonDecode(_indexFile.readAsStringSync()) as Map<String, dynamic>;
        json.forEach((k, v) => _index[k] = v as int);
      }
    } catch (_) {
      // Corrupt index: treat as empty; packs will re-download.
      _index.clear();
    }
  }

  Future<void> _saveIndex() async {
    await cacheDir.create(recursive: true);
    await _indexFile.writeAsString(jsonEncode(_index));
  }

  int? cachedVersion(String seasonId) => _index[seasonId];

  /// Checks the manifest and downloads anything new. Returns true when at
  /// least one pack was added or updated.
  Future<bool> sync() async {
    final RemoteManifest manifest;
    try {
      final res = await _client
          .get(Uri.parse('$baseUrl/manifest.json'))
          .timeout(const Duration(seconds: 10));
      if (res.statusCode != 200) return false;
      manifest = RemoteManifest.fromJson(
          jsonDecode(res.body) as Map<String, dynamic>);
    } catch (_) {
      return false;
    }

    var changed = false;
    for (final season in manifest.seasons) {
      if (_index[season.seasonId] == season.version) continue;
      if (await _downloadPack(season)) changed = true;
    }
    return changed;
  }

  Future<bool> _downloadPack(ManifestSeason season) async {
    // seasonId becomes a filesystem path segment; only accept safe slugs so a
    // hostile manifest can never write outside the cache dir.
    if (!RegExp(r'^[a-zA-Z0-9_-]+$').hasMatch(season.seasonId)) return false;

    if (season.packUrl.contains('..') ||
        season.packUrl.startsWith('/') ||
        season.packUrl.contains('://')) {
      return false;
    }

    try {
      final res = await _client
          .get(Uri.parse('$baseUrl/${season.packUrl}'))
          .timeout(const Duration(seconds: 30));
      if (res.statusCode != 200) return false;

      // Packs are tens of KB; anything huge is a server error or an attack.
      if (res.bodyBytes.length > 5 * 1024 * 1024) return false;

      // Verify in memory before any file is touched, so a bad pack can
      // never leave artifacts on disk.
      final digest = sha256.convert(res.bodyBytes).toString();
      if (digest != season.sha256) return false;

      final dir = Directory('${cacheDir.path}/seasons/${season.seasonId}');
      await dir.create(recursive: true);
      final tmp = File('${dir.path}/pack.json.tmp');
      await tmp.writeAsBytes(res.bodyBytes, flush: true);
      await tmp.rename('${dir.path}/pack.json');

      _index[season.seasonId] = season.version;
      await _saveIndex();
      return true;
    } catch (_) {
      return false;
    }
  }
}
