import 'dart:convert';

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

import '../../errors/server_exception.dart';
import '../nextcloud_ocs.dart';
import 'file_sharing_api_params.dart';
import 'queries/share/share.dart';
import 'queries/share/share_update_params.dart';

/// OCS `files_sharing` API (Nextcloud Sharing API v1). Per the official docs:
/// list/delete pass parameters via the URL (query / path), create and update
/// pass them in the request **body** (form-urlencoded), and every call adds
/// `format=json` so the server replies with JSON instead of XML.
///
/// All calls surface failures as [ServerException] so they map to friendly
/// messages via `errorToUserMessage`.
class FileSharingApi {
  static const String _base = 'apps/files_sharing/api/v1/shares';

  /// Creates a share. Returns the created [Share] (callers that don't need it —
  /// e.g. Talk file sharing — can ignore the result).
  Future<Share> share(FileSharingApiParams query) async {
    final endpoint = NextcloudOcs.uri(
      _base,
      queryParameters: {'format': 'json'},
    );
    final response = await http.post(
      endpoint,
      headers: NextcloudOcs.headers(),
      body: _stringForm(query.toJson()),
    );
    return _decodeShare(response, action: 'Freigabe erstellen');
  }

  /// Lists shares for the given OCS [path] (see `ocs_path.dart`). [reshares]
  /// includes shares the current user received and re-shared.
  Future<List<Share>> listForPath(String path, {bool reshares = false}) async {
    final endpoint = NextcloudOcs.uri(
      _base,
      queryParameters: {
        'format': 'json',
        'path': path,
        'reshares': reshares.toString(),
        'subfiles': 'false',
      },
    );
    final response = await http.get(endpoint, headers: NextcloudOcs.headers());
    final data = _decodeData(response, action: 'Freigaben laden');
    if (data is! List) return const [];
    return data
        .whereType<Map<String, dynamic>>()
        .map(Share.fromJson)
        .toList(growable: false);
  }

  /// Updates an existing share. Returns the updated [Share].
  Future<Share> update(int shareId, ShareUpdateParams params) async {
    final endpoint = NextcloudOcs.uri(
      '$_base/$shareId',
      queryParameters: {'format': 'json'},
    );
    final response = await http.put(
      endpoint,
      headers: NextcloudOcs.headers(),
      body: params.toQuery(),
    );
    return _decodeShare(response, action: 'Freigabe ändern');
  }

  /// Deletes (revokes) a share.
  Future<void> remove(int shareId) async {
    final endpoint = NextcloudOcs.uri(
      '$_base/$shareId',
      queryParameters: {'format': 'json'},
    );
    final response = await http.delete(
      endpoint,
      headers: NextcloudOcs.headers(),
    );
    _decodeData(response, action: 'Freigabe löschen');
  }

  /// Stringifies a json map into form fields (the OCS body is
  /// `application/x-www-form-urlencoded`). Null values are already dropped by
  /// the params' `includeIfNull: false`.
  Map<String, String> _stringForm(Map<String, dynamic> json) =>
      json.map((key, value) => MapEntry(key, value.toString()));

  /// Decodes a single-share response (create/update). Throws if the payload is
  /// not a share object.
  Share _decodeShare(http.Response response, {required String action}) {
    final data = _decodeData(response, action: action);
    if (data is! Map<String, dynamic>) {
      throw ServerException(
        statusCode: response.statusCode,
        technicalDetails: 'Unerwartete Antwort für "$action": ${response.body}',
      );
    }
    return Share.fromJson(data);
  }

  /// Validates the HTTP/OCS envelope and returns the `ocs.data` payload, or
  /// throws a [ServerException] carrying the server's OCS message when present.
  ///
  /// Every access is type-checked rather than cast: PHP's `json_encode` turns
  /// an empty associative array into a JSON array (`[]`), and `statuscode` can
  /// arrive as a string — a hard `as` cast on either would throw a raw
  /// TypeError that surfaces as the generic "something went wrong" message.
  Object? _decodeData(http.Response response, {required String action}) {
    dynamic decoded;
    try {
      decoded = jsonDecode(response.body);
    } catch (_) {
      decoded = null;
    }
    final ocs = decoded is Map<String, dynamic> ? decoded['ocs'] : null;
    final ocsMap = ocs is Map<String, dynamic> ? ocs : null;
    final meta = ocsMap?['meta'];
    final metaMap = meta is Map<String, dynamic> ? meta : null;

    final rawStatus = metaMap?['statuscode'];
    final ocsStatus = rawStatus is int
        ? rawStatus
        : (rawStatus is String ? int.tryParse(rawStatus) : null);
    final rawMessage = metaMap?['message'];
    final ocsMessage = rawMessage is String ? rawMessage : null;

    // OCS v2 mirrors the HTTP status; accept any 2xx. Success OCS statuscodes
    // are 100 (v1 carry-over) or 200.
    final httpOk = response.statusCode >= 200 && response.statusCode < 300;
    final ocsOk = ocsStatus == null || ocsStatus == 100 || ocsStatus == 200;
    if (!httpOk || !ocsOk) {
      throw ServerException(
        statusCode: response.statusCode,
        userMessage: ocsMessage != null && ocsMessage.isNotEmpty
            ? '$action fehlgeschlagen: $ocsMessage'
            : null,
        technicalDetails: '$action: ${response.statusCode} ${response.body}',
      );
    }
    return ocsMap?['data'];
  }
}
