// Module-store manager — list the configured module stores (+ the
// bundled seed), add a store by index URL, or remove one. The hub merges
// every store's modules into one searchable index, so adding a store
// (e.g. a domain app's published modules) makes them appear in the Store.

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

import 'package:chain_client_sdk/chain_client_sdk.dart';
import 'package:flutter/material.dart';

import '../data/error_presentation.dart';
import '../data/hub.dart';
import '../l10n/app_localizations.dart';
import '../theme/theme.dart';
import '../theme/tokens.dart';

class ChainStoresDialog extends StatefulWidget {
  const ChainStoresDialog._();

  static Future<void> show(BuildContext context) => showDialog(
        context: context,
        builder: (_) => const ChainStoresDialog._(),
      );

  @override
  State<ChainStoresDialog> createState() => _ChainStoresDialogState();
}

class _ChainStoresDialogState extends State<ChainStoresDialog> {
  late Future<List<StoreSource>> _future;
  final _name = TextEditingController();
  final _url = TextEditingController();
  final _pubkey = TextEditingController();
  bool _busy = false;

  /// Reachability of each suggested store's index URL. `null` while the
  /// probe is in flight, `true` once a response arrives that the hub
  /// could parse (fail-open: network errors also count as `true`, so a
  /// transient hiccup never hides a real store), `false` only on a
  /// definitive 4xx/5xx — i.e. "the provider hasn't published it yet".
  final Map<String, bool?> _reachable = {};

  @override
  void initState() {
    super.initState();
    _reload();
    for (final s in _kSuggestedStores) {
      _reachable[s.url] = null;
      _probe(s.url);
    }
  }

  /// One-shot HEAD-ish probe of a suggested store's index. Fails open:
  /// only a clean 4xx/5xx marks it unavailable; connection errors and
  /// timeouts leave it available so the hub stays the real authority.
  Future<void> _probe(String url) async {
    if (!url.startsWith('http')) {
      if (mounted) setState(() => _reachable[url] = true);
      return;
    }
    bool available = true;
    try {
      final client = HttpClient()
        ..connectionTimeout = const Duration(seconds: 4);
      final req = await client.getUrl(Uri.parse(url));
      final resp = await req.close().timeout(const Duration(seconds: 5));
      available = resp.statusCode >= 200 && resp.statusCode < 400;
      await resp.drain<void>();
      client.close(force: true);
    } catch (_) {
      // Offline / DNS / timeout — fail open. The add path will surface
      // any real error if the operator clicks through.
      available = true;
    }
    if (mounted) setState(() => _reachable[url] = available);
  }

  void _reload() =>
      setState(() => _future = HubService.instance.listStores());

  @override
  void dispose() {
    _name.dispose();
    _url.dispose();
    _pubkey.dispose();
    super.dispose();
  }

  Future<void> _add() => _addStore(
        _name.text.trim(),
        _url.text.trim(),
        pinnedPubkeyPem: _pubkey.text.trim(),
        fromFields: true,
      );

  /// Shared add path — used by both the manual name/URL form and the
  /// one-click suggested-store buttons.
  Future<void> _addStore(String name, String url,
      {String pinnedPubkeyPem = '', bool fromFields = false}) async {
    if (name.isEmpty || url.isEmpty) return;
    setState(() => _busy = true);
    try {
      final r = await HubService.instance
          .addStore(name: name, url: url, pinnedPubkeyPem: pinnedPubkeyPem);
      if (!mounted) return;
      if (fromFields) {
        _name.clear();
        _url.clear();
        _pubkey.clear();
      }
      ScaffoldMessenger.of(context)
          .showSnackBar(SnackBar(content: Text(r.message)));
      _reload();
    } catch (e) {
      if (mounted) {
        showFaiErrorSnack(context, 'store.add', e,
            title: AppLocalizations.of(context)!.storesManagerAddFailed);
      }
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  Future<void> _remove(String name) async {
    try {
      final r = await HubService.instance.removeStore(name);
      if (!mounted) return;
      ScaffoldMessenger.of(context)
          .showSnackBar(SnackBar(content: Text(r.message)));
      _reload();
    } catch (e) {
      if (mounted) {
        showFaiErrorSnack(context, 'store.remove', e,
            title: AppLocalizations.of(context)!.storesManagerRemoveFailed);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return AlertDialog(
      title: Text(l.storesManagerTitle),
      content: SizedBox(
        width: 540,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              l.storesManagerIntro,
              style: theme.textTheme.bodySmall
                  ?.copyWith(color: theme.colorScheme.onSurfaceVariant),
            ),
            const SizedBox(height: ChainSpace.md),
            Flexible(
              child: FutureBuilder<List<StoreSource>>(
                future: _future,
                builder: (c, snap) {
                  if (snap.connectionState == ConnectionState.waiting) {
                    return const Padding(
                      padding: EdgeInsets.all(16),
                      child: Center(child: CircularProgressIndicator()),
                    );
                  }
                  final stores = snap.data ?? const <StoreSource>[];
                  return ListView(
                    shrinkWrap: true,
                    children: [
                      for (final s in stores)
                        ListTile(
                          dense: true,
                          leading: Icon(
                            s.removable
                                ? Icons.cloud_outlined
                                : Icons.inventory_2_outlined,
                            size: 18,
                          ),
                          title: Text(s.name),
                          subtitle: Text(
                            s.url.isEmpty
                                ? l.storesManagerBundled(s.moduleCount)
                                : '${s.url}\n${l.storesManagerModuleCount(s.moduleCount)}',
                          ),
                          isThreeLine: s.url.isNotEmpty,
                          trailing: s.removable
                              ? IconButton(
                                  icon: const Icon(Icons.delete_outline),
                                  tooltip: l.buttonRemove,
                                  onPressed: () => _remove(s.name),
                                )
                              : const Icon(Icons.lock_outline, size: 16),
                        ),
                    ],
                  );
                },
              ),
            ),
            const Divider(),
            // One-click curated stores — add or remove a known store
            // (e.g. a domain app's published modules) without typing a
            // URL. State follows the configured list above.
            Text(l.storesManagerSuggested, style: theme.textTheme.titleSmall),
            const SizedBox(height: ChainSpace.sm),
            FutureBuilder<List<StoreSource>>(
              future: _future,
              builder: (c, snap) {
                final stores = snap.data ?? const <StoreSource>[];
                final configuredNameByUrl = <String, String>{
                  for (final s in stores)
                    if (s.url.isNotEmpty) s.url: s.name,
                };
                return Column(
                  children: [
                    for (final sg in _kSuggestedStores)
                      _SuggestedStoreRow(
                        store: sg,
                        configuredName: configuredNameByUrl[sg.url],
                        available: _reachable[sg.url],
                        busy: _busy,
                        onAdd: () => _addStore(sg.name, sg.url),
                        onRemove: (name) => _remove(name),
                      ),
                  ],
                );
              },
            ),
            const SizedBox(height: ChainSpace.md),
            const Divider(),
            Text(l.storesManagerAddSection, style: theme.textTheme.titleSmall),
            const SizedBox(height: ChainSpace.sm),
            Row(
              children: [
                Expanded(
                  flex: 2,
                  child: TextField(
                    controller: _name,
                    decoration: InputDecoration(
                      labelText: l.storesManagerNameLabel,
                      isDense: true,
                    ),
                  ),
                ),
                const SizedBox(width: ChainSpace.sm),
                Expanded(
                  flex: 4,
                  child: TextField(
                    controller: _url,
                    decoration: InputDecoration(
                      labelText: l.storesManagerUrlLabel,
                      isDense: true,
                    ),
                    onSubmitted: (_) => _add(),
                  ),
                ),
              ],
            ),
            const SizedBox(height: ChainSpace.sm),
            // Optional per-store pinned publisher key (PEM). When set,
            // signed modules from this store verify against this key
            // instead of the built-in vendor anchor.
            TextField(
              controller: _pubkey,
              minLines: 1,
              maxLines: 4,
              style: ChainTheme.mono(size: 11),
              decoration: InputDecoration(
                labelText: l.storesManagerPinnedKeyLabel,
                helperText: l.storesManagerPinnedKeyHint,
                helperMaxLines: 3,
                isDense: true,
              ),
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: Text(l.buttonClose),
        ),
        FilledButton(
          onPressed: _busy ? null : _add,
          child: _busy
              ? const SizedBox(
                  width: 16,
                  height: 16,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              : Text(l.storesManagerAddAction),
        ),
      ],
    );
  }
}

/// A curated, one-click store the operator can add or remove without
/// typing a URL. The hub validates the index on add, so the entry
/// only succeeds once its index is actually published at [url].
class _SuggestedStore {
  final String name;
  final String url;
  final String Function(AppLocalizations) describe;
  const _SuggestedStore({
    required this.name,
    required this.url,
    required this.describe,
  });
}

/// Known stores offered as quick-adds. Keep this short and curated —
/// it is the in-product shelf of trusted domain stores. The URL is the
/// canonical publish location each store owner commits to (see the
/// cross-agent handoff log for Recl∆Im's).
final List<_SuggestedStore> _kSuggestedStores = <_SuggestedStore>[
  _SuggestedStore(
    name: 'Recl∆Im',
    // Raw store-index file in the public chain-modules/reclaim repo —
    // stable (no release-tag churn), updated by a plain git push as
    // reclaim adds modules. The hub fetches it via StoreIndex::from_url.
    url: 'https://git.flemming.ai/chain-modules/reclaim/raw/branch/main/store.yaml',
    describe: (l) => l.storesSuggestedReclaimDesc,
  ),
];

/// One row in the suggested-stores shelf: icon, name, description, and
/// a single Add/Remove button that flips based on whether the store is
/// already configured.
class _SuggestedStoreRow extends StatelessWidget {
  final _SuggestedStore store;

  /// The configured store's name when this suggestion is already
  /// added (matched by URL), else null.
  final String? configuredName;

  /// Index reachability: null = still probing, true = reachable,
  /// false = a definitive 4xx/5xx (not published yet).
  final bool? available;
  final bool busy;
  final VoidCallback onAdd;
  final ValueChanged<String> onRemove;

  const _SuggestedStoreRow({
    required this.store,
    required this.configuredName,
    required this.available,
    required this.busy,
    required this.onAdd,
    required this.onRemove,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final added = configuredName != null;
    // Not-yet-published: a definitive bad response and not already
    // configured. Show a muted "not available yet" chip instead of an
    // Add button that would only fail on click.
    final notPublished = !added && available == false;
    final probing = !added && available == null;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          Icon(
            Icons.storefront_outlined,
            size: 18,
            color: theme.colorScheme.primary,
          ),
          const SizedBox(width: ChainSpace.sm),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  store.name,
                  style: theme.textTheme.bodyMedium
                      ?.copyWith(fontWeight: FontWeight.w600),
                ),
                Text(
                  store.describe(l),
                  style: theme.textTheme.bodySmall
                      ?.copyWith(color: theme.colorScheme.onSurfaceVariant),
                ),
              ],
            ),
          ),
          const SizedBox(width: ChainSpace.sm),
          if (added)
            OutlinedButton.icon(
              icon: const Icon(Icons.check, size: 14),
              label: Text(l.buttonRemove),
              onPressed: busy ? null : () => onRemove(configuredName!),
              style: OutlinedButton.styleFrom(
                visualDensity: VisualDensity.compact,
                foregroundColor: ChainColors.success,
              ),
            )
          else if (probing)
            const SizedBox(
              width: 16,
              height: 16,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          else if (notPublished)
            Tooltip(
              message: l.storesManagerNotAvailableHint,
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    Icons.schedule_outlined,
                    size: 14,
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                  const SizedBox(width: 4),
                  Text(
                    l.storesManagerNotAvailable,
                    style: theme.textTheme.bodySmall
                        ?.copyWith(color: theme.colorScheme.onSurfaceVariant),
                  ),
                ],
              ),
            )
          else
            FilledButton.tonalIcon(
              icon: const Icon(Icons.add, size: 14),
              label: Text(l.storesManagerAddShort),
              onPressed: busy ? null : onAdd,
              style: FilledButton.styleFrom(
                visualDensity: VisualDensity.compact,
              ),
            ),
        ],
      ),
    );
  }
}
