import 'dart:convert';

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

import '../../../model/endpoint_data.dart';
import '../../http_errors.dart';
import '../../marianumconnect/auth/device_token_name.dart';

/// Nextcloud Login Flow v2 (`/index.php/login/v2`): interactive browser login
/// that yields an app password. It is the only way to obtain working Nextcloud
/// credentials when the account is protected by two-factor authentication —
/// Basic auth with the real password is rejected server-side in that case.
class LoginFlowApi {
  final http.Client _client;

  LoginFlowApi({http.Client? client}) : _client = client ?? http.Client();

  /// Starts a new flow. Nextcloud displays the request's User-Agent as the
  /// token name in the user's security settings, so the device token label is
  /// sent (`"Marianum Fulda App (Pixel 10)"`).
  Future<LoginFlowInit> start() async {
    final userAgent = await DeviceTokenName.resolve();
    final uri = _initUri();
    const label = 'Nextcloud login flow init';
    final response = (await sendGuarded(
      label,
      () => _client.post(
        uri,
        headers: {'Accept': 'application/json', 'User-Agent': userAgent},
      ),
    ))!;
    if (response.statusCode < 200 || response.statusCode >= 300) {
      throwForStatus(
        response.statusCode,
        httpErrorDetail(label, response.body, response.statusCode),
      );
    }
    return LoginFlowInit.fromJson(
      jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>,
    );
  }

  /// Polls for the flow result: `null` while the browser login has not been
  /// completed yet (HTTP 404), the final credentials once it has.
  Future<LoginFlowCredentials?> poll(LoginFlowInit flow) async {
    const label = 'Nextcloud login flow poll';
    final response = (await sendGuarded(
      label,
      () => _client.post(
        Uri.parse(flow.pollEndpoint),
        headers: {'Accept': 'application/json'},
        body: {'token': flow.pollToken},
      ),
    ))!;
    if (response.statusCode == 404) return null;
    if (response.statusCode < 200 || response.statusCode >= 300) {
      throwForStatus(
        response.statusCode,
        httpErrorDetail(label, response.body, response.statusCode),
      );
    }
    return LoginFlowCredentials.fromJson(
      jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>,
    );
  }

  static Uri _initUri() {
    final endpoint = EndpointData().nextcloud();
    return Uri.https(endpoint.domain, '${endpoint.path}/index.php/login/v2');
  }

  /// Whether the login name reported by the completed flow belongs to the
  /// account this app session expects — the browser login could have been
  /// completed with a different Nextcloud account.
  static bool loginNameMatches({
    required String expected,
    required String actual,
  }) => actual.trim().toLowerCase() == expected.trim().toLowerCase();
}

/// Response of the flow init call: the URL the user opens in the browser plus
/// the token/endpoint pair the app polls until the login is confirmed.
class LoginFlowInit {
  final String loginUrl;
  final String pollToken;
  final String pollEndpoint;

  const LoginFlowInit({
    required this.loginUrl,
    required this.pollToken,
    required this.pollEndpoint,
  });

  factory LoginFlowInit.fromJson(Map<String, dynamic> json) {
    final poll = json['poll'];
    final loginUrl = json['login'] as String?;
    final token = poll is Map ? poll['token'] as String? : null;
    final endpoint = poll is Map ? poll['endpoint'] as String? : null;
    if (loginUrl == null || loginUrl.isEmpty) {
      throw const FormatException('login flow init: missing login url');
    }
    if (token == null || token.isEmpty || endpoint == null || endpoint.isEmpty) {
      throw const FormatException('login flow init: missing poll token/endpoint');
    }
    return LoginFlowInit(
      loginUrl: loginUrl,
      pollToken: token,
      pollEndpoint: endpoint,
    );
  }
}

/// Credentials returned once the user confirmed the login in the browser.
class LoginFlowCredentials {
  final String server;
  final String loginName;
  final String appPassword;

  const LoginFlowCredentials({
    required this.server,
    required this.loginName,
    required this.appPassword,
  });

  factory LoginFlowCredentials.fromJson(Map<String, dynamic> json) {
    final loginName = json['loginName'] as String?;
    final appPassword = json['appPassword'] as String?;
    if (loginName == null || loginName.isEmpty) {
      throw const FormatException('login flow poll: missing loginName');
    }
    if (appPassword == null || appPassword.isEmpty) {
      throw const FormatException('login flow poll: missing appPassword');
    }
    return LoginFlowCredentials(
      server: json['server'] as String? ?? '',
      loginName: loginName,
      appPassword: appPassword,
    );
  }
}
