import "dart:convert";

import "package:flutter/foundation.dart";
import "package:shared_preferences/shared_preferences.dart";

import "config.dart";
import "i18n.dart";
import "models.dart";

/// The phone can be pointed at more than one DriverVault API Server. One is the
/// "home" server — the address this build ships with, or whatever the login
/// screen's Server settings overrode it to — and any number of others are added
/// by URL, which works because an API Server a phone can reach is
/// internet-facing already.
///
/// Each server is its own PocketBase with its own users, so a session cannot be
/// carried across: every server holds its own token under its own key. Only one
/// is active at a time and the whole app reads from it, so switching swaps the
/// garage, the charging page and the settings together.
///
/// Mirrors the web app's servers.js, down to the storage keys — the same three
/// legacy ones are read once at boot so an upgrade doesn't sign anyone out or
/// forget the server they had pointed the app at.

const String kHomeServerId = "home";

/// One server the app can read from. An empty [url] means [kDefaultApiBase],
/// which only the home entry is allowed — anything else without an address is
/// unusable.
class ServerEntry {
  final String id;
  String name;
  String url;

  ServerEntry({required this.id, this.name = "", this.url = ""});

  bool get isHome => id == kHomeServerId;

  Map<String, dynamic> toJson() => {"id": id, "name": name, "url": url};

  factory ServerEntry.fromJson(Map<String, dynamic> json) => ServerEntry(
        id: json["id"].toString(),
        name: (json["name"] ?? "").toString(),
        url: normalizeServerUrl((json["url"] ?? "").toString()),
      );
}

/// What is held for a signed-in server: PocketBase's own token and the user it
/// was minted for.
class ServerSession {
  final String token;
  final AuthUser? user;
  const ServerSession(this.token, this.user);
}

/// A server address as typed. Trailing slashes go, and a bare origin gets
/// "/api" appended — every API Server route lives under it, so entering just
/// "https://garage.example.com" should work without the user knowing that.
String normalizeServerUrl(String url) {
  final trimmed = url.trim().replaceAll(RegExp(r"/+$"), "");
  if (trimmed.isEmpty) return "";
  final parsed = Uri.tryParse(trimmed);
  if (parsed != null &&
      parsed.hasScheme &&
      parsed.hasAuthority &&
      (parsed.path.isEmpty || parsed.path == "/")) {
    return "${parsed.scheme}://${parsed.authority}/api";
  }
  return trimmed;
}

class ServerRegistry extends ChangeNotifier {
  static const _listKey = "cc_servers";
  static const _activeKey = "cc_active_server";
  static String _sessionKey(String id) => "cc_session_$id";

  /// The active server's resolved base URL, written out for the Android Auto
  /// service to read (android/.../car/VaultStore.kt). The car screens run
  /// without a Flutter engine, so they read this store directly — and an
  /// untouched home entry carries no address of its own to read, its base being
  /// [kDefaultApiBase], a compile-time define nothing outside Dart can see.
  static const _activeBaseKey = "cc_active_base";

  // Pre-multi-server keys, read once by [_migrateLegacy].
  static const _legacyTokenKey = "cc_token";
  static const _legacyUserKey = "cc_user";
  static const _legacyServerKey = "cc_server_url";

  final List<ServerEntry> list = [];
  String activeId = kHomeServerId;
  final Map<String, ServerSession> _sessions = {};

  // Captured once in [load] so every later write is a plain call rather than an
  // await: the registry is read on the way into every request, and a switch has
  // to be in effect the moment it returns.
  SharedPreferences? _prefs;

  /// Reads the stored servers and their sessions. Call once in main() before
  /// the first request; everything below is synchronous afterwards.
  Future<void> load() async {
    final prefs = await SharedPreferences.getInstance();
    _prefs = prefs;
    _migrateLegacy();
    list
      ..clear()
      ..addAll(_loadList());
    _sessions.clear();
    for (final server in list) {
      final session = _loadSession(server.id);
      if (session != null) _sessions[server.id] = session;
    }
    final stored = prefs.getString(_activeKey);
    activeId = list.any((s) => s.id == stored) ? stored! : kHomeServerId;
    _syncActiveBase();
  }

  /// Republishes the active server's base URL for the car screens. Called
  /// wherever which server is active, or where it answers, can have changed.
  void _syncActiveBase() => _prefs?.setString(_activeBaseKey, activeBase);

  List<ServerEntry> _loadList() {
    final raw = _prefs?.getString(_listKey);
    final decoded = raw == null ? null : _tryDecode(raw);
    final entries = <ServerEntry>[];
    if (decoded is List) {
      for (final item in decoded) {
        if (item is Map && item["id"] != null) {
          final entry = ServerEntry.fromJson(Map<String, dynamic>.from(item));
          if (entry.isHome || entry.url.isNotEmpty) entries.add(entry);
        }
      }
    }
    if (!entries.any((s) => s.isHome)) {
      entries.insert(0, ServerEntry(id: kHomeServerId));
    }
    return entries;
  }

  ServerSession? _loadSession(String id) {
    final raw = _prefs?.getString(_sessionKey(id));
    final decoded = raw == null ? null : _tryDecode(raw);
    if (decoded is! Map || decoded["token"] == null) return null;
    final user = decoded["user"];
    return ServerSession(
      decoded["token"].toString(),
      user is Map ? AuthUser.fromJson(Map<String, dynamic>.from(user)) : null,
    );
  }

  /// Carries a pre-multi-server session onto the home server. Any override the
  /// user had set in the login screen's Server settings becomes home's URL, so
  /// the same server keeps answering after the upgrade.
  void _migrateLegacy() {
    final prefs = _prefs!;
    final token = prefs.getString(_legacyTokenKey);
    final legacyUrl = prefs.getString(_legacyServerKey);
    if (token == null && legacyUrl == null) return;
    if (token != null && prefs.getString(_sessionKey(kHomeServerId)) == null) {
      final rawUser = prefs.getString(_legacyUserKey);
      final user = rawUser == null ? null : _tryDecode(rawUser);
      prefs.setString(
        _sessionKey(kHomeServerId),
        jsonEncode({"token": token, "user": user is Map ? user : null}),
      );
    }
    if (legacyUrl != null && legacyUrl.isNotEmpty && prefs.getString(_listKey) == null) {
      prefs.setString(
        _listKey,
        jsonEncode([ServerEntry(id: kHomeServerId, url: normalizeServerUrl(legacyUrl)).toJson()]),
      );
    }
    prefs.remove(_legacyTokenKey);
    prefs.remove(_legacyUserKey);
    prefs.remove(_legacyServerKey);
  }

  void _saveList() =>
      _prefs?.setString(_listKey, jsonEncode(list.map((s) => s.toJson()).toList()));

  // --- reading -------------------------------------------------------------

  ServerEntry get active =>
      byId(activeId) ?? (list.isNotEmpty ? list.first : ServerEntry(id: kHomeServerId));

  ServerEntry? byId(String id) {
    for (final server in list) {
      if (server.id == id) return server;
    }
    return null;
  }

  /// The base URL requests against [server] go to.
  String baseFor(ServerEntry? server) {
    final url = server?.url.trim() ?? "";
    return url.isEmpty ? kDefaultApiBase : url;
  }

  String get activeBase => baseFor(active);

  ServerSession? sessionFor(String id) => _sessions[id];

  ServerSession? get activeSession => _sessions[activeId];

  String? get activeToken => activeSession?.token;

  bool isConnected(String id) => _sessions[id] != null;

  /// What to call a server in the UI: the name it was given, else the host it
  /// answers on, else — for an untouched home entry — a generic label.
  String displayName(ServerEntry? server) {
    if (server == null) return "";
    if (server.name.isNotEmpty) return server.name;
    if (server.url.isEmpty) return t("servers.home");
    final host = Uri.tryParse(server.url)?.host ?? "";
    return host.isEmpty ? server.url : host;
  }

  // --- writing -------------------------------------------------------------

  void setSession(String id, String token, AuthUser? user) {
    _sessions[id] = ServerSession(token, user);
    _prefs?.setString(
      _sessionKey(id),
      jsonEncode({"token": token, "user": user?.toJson()}),
    );
    notifyListeners();
  }

  void clearSession(String id) {
    _sessions.remove(id);
    _prefs?.remove(_sessionKey(id));
    notifyListeners();
  }

  void clearAllSessions() {
    for (final server in list) {
      _sessions.remove(server.id);
      _prefs?.remove(_sessionKey(server.id));
    }
    notifyListeners();
  }

  void setActive(String id) {
    if (byId(id) == null || id == activeId) return;
    activeId = id;
    _prefs?.setString(_activeKey, id);
    _syncActiveBase();
    notifyListeners();
  }

  ServerEntry add({String name = "", String url = ""}) {
    final server = ServerEntry(
      id: "s${DateTime.now().microsecondsSinceEpoch.toRadixString(36)}",
      name: name.trim(),
      url: normalizeServerUrl(url),
    );
    list.add(server);
    _saveList();
    notifyListeners();
    return server;
  }

  ServerEntry? update(String id, {String? name, String? url}) {
    final server = byId(id);
    if (server == null) return null;
    if (name != null) server.name = name.trim();
    if (url != null) {
      final next = normalizeServerUrl(url);
      // Moving a server to a different address invalidates the token held for
      // it: it was minted by the PocketBase behind the old one.
      if (next != server.url) clearSession(id);
      server.url = next;
    }
    _saveList();
    _syncActiveBase();
    notifyListeners();
    return server;
  }

  /// Removing a server forgets its session too. Home cannot be removed — it is
  /// the address the app ships with, and there would be nothing left to fall
  /// back to.
  void remove(String id) {
    if (id == kHomeServerId) return;
    clearSession(id);
    list.removeWhere((s) => s.id == id);
    _saveList();
    if (activeId == id) {
      activeId = kHomeServerId;
      _prefs?.setString(_activeKey, kHomeServerId);
    }
    _syncActiveBase();
    notifyListeners();
  }

  static dynamic _tryDecode(String raw) {
    try {
      return jsonDecode(raw);
    } catch (_) {
      return null;
    }
  }
}

/// App-wide singleton (mirrors biometricAuth in biometric.dart).
final serverRegistry = ServerRegistry();
