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

import 'package:commons_core/commons_core.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

import '../data/species_catalog.dart';
import '../data/species_repository.dart';
import '../data/variety_repository.dart';
import '../db/database.dart';
import '../db/encrypted_executor.dart';
import '../i18n/strings.g.dart';
import '../security/secret_store.dart';
import '../security/secure_key_store.dart';
import '../services/auto_backup_service.dart';
import '../services/auto_backup_store.dart';
import '../services/device_id_store.dart';
import '../services/export_import_service.dart';
import '../services/file_picker_file_service.dart';
import '../services/file_service.dart';
import '../services/inbox_service.dart';
import '../services/locale_store.dart';
import '../services/notification_service.dart';
import '../services/ocr/label_text_extractor.dart';
import '../services/ocr/ocr_language.dart';
import '../services/ocr/tesseract_label_extractor.dart';
import '../services/label_sheet_service.dart';
import '../services/onboarding_store.dart';
import '../services/recovery_sheet_service.dart';
import '../services/share_catalog_service.dart';
import '../services/message_store.dart';
import '../services/offer_outbox.dart';
import '../services/profile_cache.dart';
import '../services/profile_store.dart';
import '../services/social_account_store.dart';
import '../services/social_connection.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';
import '../services/sync_service.dart';
import '../services/trust_referents.dart';
import '../services/unread_service.dart';
import '../services/wot_settings.dart';

/// The app's service locator. Kept to the composition root — widgets get their
/// repositories from here (or via BlocProvider), never by reaching into it deep
/// in the tree.
final GetIt getIt = GetIt.instance;

/// End-of-init sentinel: registered last, so `isRegistered<_DepsReady>()` means
/// the container is fully wired (see [configureDependencies]). Distinct from
/// [SocialService], which is optional and absent in inventory-only mode.
class _DepsReady {
  const _DepsReady();
}

/// Wires the encrypted DB, keystore and repositories. Call once from `main`
/// before `runApp`; the DB key must exist before the DB opens.
///
/// Idempotent AND all-or-nothing: a hot restart (or an Android Activity restart
/// that keeps the isolate) re-runs `main` while GetIt still holds the singletons.
///
/// A dedicated [_DepsReady] marker is registered LAST and used as the "fully
/// wired" sentinel, so `isRegistered<_DepsReady>()` means the WHOLE container is
/// ready — never a half-built one. If a prior run crashed part-way through init
/// (leaving some singletons registered but not the marker), we reset the
/// container and rebuild from scratch rather than early-returning on a partial
/// container or throwing "already registered" mid-cascade. That partial state is
/// safe to reset because `main` aborts before `runApp` on a partial run, so no
/// live UI holds these singletons. Without this, the app intermittently fell
/// back to the "inventory only" look (market/chat/profile greyed out).
Future<void> configureDependencies() async {
  if (getIt.isRegistered<_DepsReady>()) return; // fully wired already
  // A prior run half-wired the container (crashed after some registrations but
  // before the sentinel). Clear it so we rebuild cleanly.
  if (getIt.isRegistered<AppDatabase>()) await getIt.reset();

  final secretStore = FlutterSecretStore();
  final keyStore = SecureKeyStore(store: secretStore);
  final dbKeyHex = await keyStore.databaseKeyHex();
  final rootSeedHex = await keyStore.rootSeedHex();

  // Which social identity ("account") of the root seed is active. Namespaces the
  // per-identity stores (chats, profile, name cache) so identities never mix.
  final accounts = SocialAccountStore(secretStore);
  final activeAccount = await accounts.active();
  final scope = socialAccountScope(activeAccount);

  // Stable per-install id for device-to-device sync (distinct from the
  // seed-derived CRDT node id, which is shared across a user's devices).
  final deviceId = await DeviceIdStore(secretStore).deviceId();

  final database = AppDatabase(
    openEncryptedExecutor(await _databaseFile(), dbKeyHex),
  );

  // CRDT author id: a stable per-install slice of the root seed. Kept distinct
  // from the social-layer public key on purpose — unifying the two would rewrite
  // authorship of existing rows, a data-model decision for later.
  final nodeId = rootSeedHex.substring(0, 16);

  // Block 2 social identity: a secp256k1 Nostr key derived (one-way) from the
  // SAME root seed, so it needs no extra backup. Cheap + offline; no relay is
  // contacted here (local-first — the social layer only enriches). Non-fatal: if
  // the derivation ever fails, the app still opens on inventory (market/chat/
  // profile stay hidden) rather than blanking the whole app.
  SocialService? socialService;
  try {
    socialService = await SocialService.fromRootSeedHex(
      rootSeedHex,
      account: activeAccount,
      deviceId: deviceId,
    );
  } catch (e, s) {
    debugPrint('Social identity derivation failed; inventory-only mode: $e\n$s');
  }

  // Seed the bundled species catalog before the UI opens — but only once per
  // catalog version. Parsing the ~1.5 MB catalog and scanning the species table
  // on every launch was the main startup jank; the version marker skips both
  // when this install already holds the current catalog.
  const catalogVersionKey = 'species_catalog_version';
  final speciesRepository = SpeciesRepository(database, idGen: IdGen());
  await speciesRepository.seedBundledIfNeeded(
    version: speciesCatalogVersion,
    readVersion: () => secretStore.read(catalogVersionKey),
    writeVersion: (v) => secretStore.write(catalogVersionKey, v),
    loadSeeds: loadBundledSpecies,
  );

  final varietyRepository = VarietyRepository(
    database,
    idGen: IdGen(),
    nodeId: nodeId,
  );

  const fileService = FilePickerFileService();

  // OCR label suggestions only where a native Tesseract engine exists; every
  // other platform (desktop, web) degrades to the no-op and hides the button.
  // The language pack(s) follow the user's locale(s) — never a fixed region —
  // limited to what we actually bundle, always unioned with the Latin baseline.
  final LabelTextExtractor labelExtractor =
      (Platform.isAndroid || Platform.isIOS)
      ? TesseractLabelExtractor(language: await _resolveOcrLanguage())
      : const NoOpLabelTextExtractor();

  getIt
    ..registerSingleton<SecureKeyStore>(keyStore)
    ..registerSingleton<AppDatabase>(database)
    ..registerSingleton<SpeciesRepository>(speciesRepository)
    ..registerSingleton<VarietyRepository>(varietyRepository)
    ..registerSingleton<FileService>(fileService)
    ..registerSingleton<LabelTextExtractor>(labelExtractor)
    ..registerSingleton<OnboardingStore>(OnboardingStore(secretStore))
    ..registerSingleton<LocaleStore>(LocaleStore(secretStore))
    ..registerSingleton<SocialSettings>(SocialSettings(secretStore))
    ..registerSingleton<SocialAccountStore>(accounts)
    // Web of trust: network-wide (not per-identity), so registered once.
    ..registerSingleton<TrustReferents>(TrustReferents(secretStore))
    ..registerSingleton<WotSettings>(WotSettings(secretStore))
    ..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
    // Per-identity stores are namespaced by the active account's scope.
    ..registerSingleton<MessageStore>(
        MessageStore(secretStore, accountScope: scope))
    ..registerSingleton<ProfileStore>(
        ProfileStore(secretStore, accountScope: scope))
    ..registerSingleton<ProfileCache>(
        ProfileCache(secretStore, accountScope: scope))
    ..registerSingleton<ExportImportService>(
      ExportImportService(
        repository: varietyRepository,
        files: fileService,
        keys: keyStore,
      ),
    )
    ..registerSingleton<ShareCatalogService>(
      ShareCatalogService(files: fileService),
    )
    ..registerSingleton<RecoverySheetService>(
      RecoverySheetService(files: fileService),
    )
    ..registerSingleton<LabelSheetService>(
      LabelSheetService(files: fileService),
    );

  // Automatic silent backups need real file storage; the web build has none, so
  // it simply goes without (the manual "save a copy" still works there).
  if (!kIsWeb) {
    getIt.registerSingleton<AutoBackupService>(
      AutoBackupService(
        exporter: getIt<ExportImportService>(),
        store: AutoBackupStore(secretStore),
        directory: _backupsDir,
      ),
    );
  }

  // OS notifications for foreground messages. Registered unconditionally — it
  // self-no-ops on unsupported platforms (web/Windows) and when the social layer
  // is absent nothing ever calls it. Its tap handler is wired to the router in
  // `TaneApp`, after the router exists.
  getIt.registerSingleton<NotificationService>(NotificationService());

  // Optional: absent only if the derivation above failed — then the app runs
  // inventory-only, by design (market/chat/profile hidden).
  if (socialService != null) {
    // ONE shared relay connection per identity, reused by every feature.
    final connection = SocialConnection(
      social: socialService,
      settings: getIt<SocialSettings>(),
    );
    getIt
      ..registerSingleton<SocialService>(socialService)
      ..registerSingleton<SocialConnection>(connection)
      // Tracks unread private messages so the UI can badge them.
      ..registerSingleton<UnreadService>(
        UnreadService(getIt<MessageStore>(), secretStore),
      )
      // App-wide inbox listener over the shared connection, so private messages
      // arrive (and land in the inbox list) even when the chat isn't open.
      // Started in `Bootstrap`; it also updates unread and fires notifications.
      ..registerSingleton<InboxService>(
        InboxService(
          connection: connection,
          selfPubkey: socialService.publicKeyHex,
          store: getIt<MessageStore>(),
          profileCache: getIt<ProfileCache>(),
          unread: getIt<UnreadService>(),
          notifications: getIt<NotificationService>(),
        ),
      )
      // Replicates the inventory across this identity's own devices over the
      // shared connection. Started in `Bootstrap`.
      ..registerSingleton<SyncService>(
        SyncService(
          connection: connection,
          io: getIt<ExportImportService>(),
          localChanges: database.tableUpdates().map<void>((_) {}),
          selfDeviceId: deviceId,
        ),
      );
  }

  // Registered LAST: the "fully wired" sentinel the guard above checks. Anything
  // that throws before here leaves the marker unregistered, so the next run
  // rebuilds cleanly instead of reusing a half-built container.
  getIt.registerSingleton<_DepsReady>(const _DepsReady());
}

/// Switches the active social identity to [account], re-deriving the Nostr key
/// from the SAME root seed and re-scoping the per-identity stores (chats,
/// profile, name cache) so nothing leaks between identities. Restarts the inbox
/// listener on the new identity. The caller must rebuild the widget tree
/// (`RestartWidget.restart`) so screens and router pick up the new singletons.
/// The DB, inventory and relay settings are untouched — only the social slice.
Future<void> switchSocialAccount(int account) async {
  final secretStore = FlutterSecretStore();
  await getIt<SocialAccountStore>().setActive(account);
  final scope = socialAccountScope(account);
  final rootSeedHex = await getIt<SecureKeyStore>().rootSeedHex();
  // Per-install, so it survives an identity switch.
  final deviceId = await DeviceIdStore(secretStore).deviceId();

  // Tear down the old identity's listener + sync + shared connection first.
  if (getIt.isRegistered<SyncService>()) {
    await getIt<SyncService>().stop();
    await getIt.unregister<SyncService>();
  }
  if (getIt.isRegistered<InboxService>()) {
    await getIt<InboxService>().stop();
    await getIt.unregister<InboxService>();
  }
  if (getIt.isRegistered<SocialConnection>()) {
    await getIt<SocialConnection>().dispose();
    await getIt.unregister<SocialConnection>();
  }
  if (getIt.isRegistered<UnreadService>()) {
    await getIt.unregister<UnreadService>();
  }
  if (getIt.isRegistered<SocialService>()) {
    await getIt.unregister<SocialService>();
  }
  await getIt.unregister<MessageStore>();
  await getIt.unregister<ProfileStore>();
  await getIt.unregister<ProfileCache>();

  // Re-register the per-identity stores under the new scope, then the identity.
  getIt
    ..registerSingleton<MessageStore>(
        MessageStore(secretStore, accountScope: scope))
    ..registerSingleton<ProfileStore>(
        ProfileStore(secretStore, accountScope: scope))
    ..registerSingleton<ProfileCache>(
        ProfileCache(secretStore, accountScope: scope))
    ..registerSingleton<UnreadService>(
        UnreadService(getIt<MessageStore>(), secretStore));

  final social = await SocialService.fromRootSeedHex(
    rootSeedHex,
    account: account,
    deviceId: deviceId,
  );
  final connection =
      SocialConnection(social: social, settings: getIt<SocialSettings>());
  final inbox = InboxService(
    connection: connection,
    selfPubkey: social.publicKeyHex,
    store: getIt<MessageStore>(),
    profileCache: getIt<ProfileCache>(),
    unread: getIt<UnreadService>(),
    notifications: getIt<NotificationService>(),
  );
  final sync = SyncService(
    connection: connection,
    io: getIt<ExportImportService>(),
    localChanges: getIt<AppDatabase>().tableUpdates().map<void>((_) {}),
    selfDeviceId: deviceId,
  );
  getIt
    ..registerSingleton<SocialService>(social)
    ..registerSingleton<SocialConnection>(connection)
    ..registerSingleton<InboxService>(inbox)
    ..registerSingleton<SyncService>(sync);
  // Subscribe the listeners BEFORE the connection starts connecting.
  inbox.start();
  sync.start();
  connection.start();
}

Future<Directory> _backupsDir() async {
  final dir = await getApplicationSupportDirectory();
  return Directory(p.join(dir.path, 'backups'));
}

Future<File> _databaseFile() async {
  final dir = await getApplicationDocumentsDirectory();
  return File(p.join(dir.path, 'tane_inventory.sqlite'));
}

/// Picks the Tesseract language pack(s) for the current locale(s), limited to
/// the bundled packs listed in `tessdata_config.json`. Prefers the app's chosen
/// language, then the device's ordered locales.
Future<String> _resolveOcrLanguage() async {
  final available = parseAvailableOcrLanguages(
    await rootBundle.loadString('assets/tessdata_config.json'),
  );
  final preferred = <String>[
    LocaleSettings.currentLocale.languageCode,
    for (final l in WidgetsBinding.instance.platformDispatcher.locales)
      l.languageCode,
  ];
  return resolveOcrLanguages(preferredLocales: preferred, available: available);
}
