import 'dart:io';
import 'package:path/path.dart' as p;

/// Operator-managed gRPC bearer token used to authenticate
/// Studio against a hub that has `auth.tokens:` configured
/// (RBAC Level 2). Stored at `~/.chain/hub-auth-token` (mode
/// 0600 on Unix). Sister of [RegistryToken] — same on-disk
/// hygiene, different secret.
///
/// When the file is present and non-empty Studio constructs
/// `HubClient(authToken: ...)`, which attaches
/// `Authorization: Bearer <token>` to every gRPC and
/// gRPC-Web call. When the file is missing Studio stays in
/// anonymous mode; the hub then either accepts or rejects
/// based on its own `auth.tokens:` config.
class HubAuthToken {
  static String _faiHome() {
    final home =
        Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
    if (home == null || home.isEmpty) {
      throw StateError('Cannot resolve home directory (no HOME / USERPROFILE)');
    }
    return p.join(home, '.fai');
  }

  /// Absolute path to the token file.
  static String get path => p.join(_faiHome(), 'hub-auth-token');

  /// True iff the file exists with a non-empty trimmed body.
  static Future<bool> isConfigured() async {
    final f = File(path);
    if (!await f.exists()) return false;
    final content = await f.readAsString();
    return content.trim().isNotEmpty;
  }

  /// Length of the trimmed token, or null when the file is
  /// missing / empty. Used for status display ("Configured
  /// (40 chars)") without ever surfacing the secret.
  static Future<int?> charCount() async {
    final f = File(path);
    if (!await f.exists()) return null;
    final content = await f.readAsString();
    final trimmed = content.trim();
    return trimmed.isEmpty ? null : trimmed.length;
  }

  /// Read the trimmed token, or `null` when missing/empty.
  /// Studio calls this at startup so [HubService] can supply
  /// it to the underlying gRPC client.
  static Future<String?> read() async {
    final f = File(path);
    if (!await f.exists()) return null;
    final content = (await f.readAsString()).trim();
    return content.isEmpty ? null : content;
  }

  /// Persist [token] to disk, creating `~/.chain/` if needed.
  ///
  /// Atomic write: writes to `<path>.tmp`, chmods 0600 on
  /// Unix BEFORE moving into place, then renames. This
  /// closes the TOCTOU window where the historical
  /// `writeAsString` + later `chmod 600` sequence left the
  /// file world-readable for the duration of the chmod call.
  ///
  /// Also sets `~/.chain/` itself to 0700 on Unix on first
  /// creation so other users on a shared host can't even
  /// enumerate the directory (the 0600 token-file mode is
  /// fine in isolation; combining it with a 0755 parent
  /// directory leaks the file's existence).
  static Future<void> write(String token) async {
    final f = File(path);
    final tmp = File('${f.path}.tmp');
    final parent = f.parent;
    final parentExisted = await parent.exists();
    await parent.create(recursive: true);
    if ((Platform.isLinux || Platform.isMacOS) && !parentExisted) {
      try {
        await Process.run('chmod', ['700', parent.path]);
      } catch (_) {
        /* best-effort */
      }
    }
    // Write to the temp path with restrictive mode set up
    // BEFORE the rename — the final file is never visible
    // to other processes with the default umask mode.
    await tmp.writeAsString(token.trim(), flush: true);
    if (Platform.isLinux || Platform.isMacOS) {
      try {
        await Process.run('chmod', ['600', tmp.path]);
      } catch (_) {
        /* best-effort */
      }
    }
    // Atomic rename: on POSIX, replaces the destination
    // in one syscall. On Windows, Dart's File.rename uses
    // MoveFileEx with MOVEFILE_REPLACE_EXISTING which is
    // equally atomic for this use case.
    try {
      await tmp.rename(f.path);
    } catch (_) {
      // On rare cross-mount setups, rename may fail; fall
      // back to copy + delete. The token is then briefly
      // visible at the .tmp path with 0600 mode — still
      // safe, but not atomic.
      await tmp.copy(f.path);
      if (Platform.isLinux || Platform.isMacOS) {
        try {
          await Process.run('chmod', ['600', f.path]);
        } catch (_) {
          /* best-effort */
        }
      }
      await tmp.delete();
    }
  }

  /// Remove the token file. Returns true iff the file existed
  /// before deletion.
  static Future<bool> clear() async {
    final f = File(path);
    if (!await f.exists()) return false;
    await f.delete();
    return true;
  }
}
