import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';

import '../models/change.dart';
import '../models/sheet.dart';

/// HTTP client for communicating with the Sheetless API server.
class ApiClient {
  final _log = Logger('ApiClient');

  final String baseUrl;
  String? accessToken; // Short-lived jwt
  String? refreshToken; // Long lived binary token

  ApiClient({required this.baseUrl, this.refreshToken});

  // ---------------------------------------------------------------------------
  // Authentication
  // ---------------------------------------------------------------------------
  Future<void> login(String username, String password) async {
    _log.info('Logging in...');

    final response = await post("/auth/login", {
      'username': username,
      'password': password,
    }, authenticateWithAccessToken: false);

    if (response.statusCode == 200) {
      final responseData = jsonDecode(response.body);
      accessToken = responseData['access_token'];
      refreshToken = responseData['refresh_token'];
      _log.info('Login successful');
    } else {
      throw ApiException(
        'Login failed',
        statusCode: response.statusCode,
        body: response.body,
      );
    }
  }

  Future<void> refreshAccessToken() async {
    _log.info('Refreshing access token...');
    final response = await post("/auth/refresh", {
      "refresh_token": refreshToken,
    }, authenticateWithAccessToken: false);
    if (response.statusCode == 200) {
      final responseData = jsonDecode(response.body);
      accessToken = responseData['access_token'];
      _log.info('Token refresh successful');
    } else {
      throw ApiException(
        'Token refresh failed',
        statusCode: response.statusCode,
        body: response.body,
      );
    }
  }

  Future<void> logout() async {
    try {
      await post(
        "/auth/logout",
        {},
        authenticateWithAccessToken: true,
      ); // Ignoring response, logging out in any case
    } catch (e) {
      _log.fine('Logout failed: $e');
    }
    accessToken = null;
    refreshToken = null;
    _log.info('Logged out successfully');
  }

  // ---------------------------------------------------------------------------
  // HTTP Helpers
  // ---------------------------------------------------------------------------

  Map<String, String> _buildHeaders({
    bool authenticateWithAccessToken = false,
    bool isBinary = false,
  }) {
    final headers = {
      if (authenticateWithAccessToken) 'Authorization': 'Bearer $accessToken',
      if (!isBinary) 'Content-Type': 'application/json',
    };
    return headers;
  }

  /// Performs a GET request to the given endpoint.
  Future<http.Response> get(
    String endpoint, {
    bool isBinary = false,
    bool authenticateWithAccessToken = true,
    bool isRetry = false,
  }) async {
    final url = Uri.parse('$baseUrl$endpoint');
    final response = await http.get(
      url,
      headers: _buildHeaders(
        isBinary: isBinary,
        authenticateWithAccessToken: authenticateWithAccessToken,
      ),
    );

    // Unauthorized, refresh access token
    if (authenticateWithAccessToken && response.statusCode == 401 && !isRetry) {
      await refreshAccessToken();
      return get(
        endpoint,
        isBinary: isBinary,
        authenticateWithAccessToken: authenticateWithAccessToken,
        isRetry: true,
      );
    }

    if (response.statusCode != 200) {
      _log.warning(
        "GET '$endpoint' failed: ${response.statusCode}\n${response.body}",
      );
      throw ApiException(
        'GET request failed',
        statusCode: response.statusCode,
        body: response.body,
      );
    }

    return response;
  }

  /// Performs a POST request with JSON body.
  Future<http.Response> post(
    String endpoint,
    Map<String, dynamic> body, {
    bool authenticateWithAccessToken = true,
    bool isRetry = false,
  }) async {
    final url = Uri.parse('$baseUrl$endpoint');

    final response = await http.post(
      url,
      headers: _buildHeaders(
        authenticateWithAccessToken: authenticateWithAccessToken,
      ),
      body: jsonEncode(body),
    );

    // Unauthorized, refresh access token
    if (authenticateWithAccessToken && response.statusCode == 401 && !isRetry) {
      await refreshAccessToken();
      return post(
        endpoint,
        body,
        authenticateWithAccessToken: authenticateWithAccessToken,
        isRetry: true,
      );
    }

    if (response.statusCode != 200 && response.statusCode != 201) {
      _log.warning(
        "POST '$endpoint' failed: ${response.statusCode}\n${response.body}",
      );
      throw ApiException(
        'POST request failed',
        statusCode: response.statusCode,
        body: response.body,
      );
    }

    return response;
  }

  /// Performs a POST request with form-encoded body.
  Future<http.Response> postFormData(
    String endpoint,
    String body, {
    bool isRetry = false,
  }) async {
    final url = Uri.parse('$baseUrl$endpoint');

    final response = await http.post(
      url,
      headers: {
        'Authorization': 'Bearer $accessToken',
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: body,
    );

    // Unauthorized, refresh access token
    if (response.statusCode == 401 && !isRetry) {
      await refreshAccessToken();
      return postFormData(endpoint, body, isRetry: true);
    }

    if (response.statusCode != 200 && response.statusCode != 201) {
      _log.warning(
        "POST Form '$endpoint' failed: ${response.statusCode}\n${response.body}",
      );
      throw ApiException(
        'POST Form request failed',
        statusCode: response.statusCode,
        body: response.body,
      );
    }

    return response;
  }

  // ---------------------------------------------------------------------------
  // Sheet Operations
  // ---------------------------------------------------------------------------

  /// Fetches the list of all sheets from the server.
  Future<List<Sheet>> fetchSheets() async {
    final response = await get('/api/sheets/list');
    final data = jsonDecode(response.body);

    return (data['sheets'] as List<dynamic>)
        .map((sheet) => Sheet.fromJson(sheet as Map<String, dynamic>))
        .toList();
  }

  /// Downloads PDF data for a sheet.
  ///
  /// Returns the raw PDF bytes. For caching, use [getPdfFileCached] instead.
  Future<Uint8List> fetchPdfData(String sheetUuid) async {
    final response = await get('/api/sheets/get/$sheetUuid', isBinary: true);
    _log.info('PDF downloaded for sheet $sheetUuid');
    return response.bodyBytes;
  }

  /// Gets a cached PDF file for a sheet, downloading if necessary.
  ///
  /// On web platforms, use [fetchPdfData] instead as file caching
  /// is not supported.
  Future<File> getPdfFileCached(String sheetUuid) async {
    final cacheDir = await getTemporaryDirectory();
    final cachedFile = File('${cacheDir.path}/$sheetUuid.pdf');

    if (await cachedFile.exists()) {
      _log.info('PDF found in cache: ${cachedFile.path}');
      return cachedFile;
    }

    final pdfData = await fetchPdfData(sheetUuid);
    await cachedFile.writeAsBytes(pdfData);
    _log.info('PDF cached at: ${cachedFile.path}');
    return cachedFile;
  }

  Future<bool> isPdfFileCached(String sheetUuid) async {
    final cacheDir = await getTemporaryDirectory();
    final cachedFile = File('${cacheDir.path}/$sheetUuid.pdf');

    return await cachedFile.exists();
  }

  // ---------------------------------------------------------------------------
  // Annotation Operations
  // ---------------------------------------------------------------------------

  /// Fetches all annotations for a sheet from the server.
  ///
  /// Returns a list of [ServerAnnotation] objects containing page number,
  /// lastModified timestamp, and the annotations JSON string.
  Future<List<ServerAnnotation>> fetchAnnotations(String sheetUuid) async {
    final response = await get('/api/sheets/$sheetUuid/annotations');
    final data = jsonDecode(response.body) as List<dynamic>;

    return data
        .map((item) => ServerAnnotation.fromJson(item as Map<String, dynamic>))
        .toList();
  }

  /// Uploads annotations for a specific page of a sheet.
  ///
  /// The [lastModified] should be the current time when the annotation was saved.
  Future<void> uploadAnnotation({
    required String sheetUuid,
    required int page,
    required DateTime lastModified,
    required String annotationsJson,
  }) async {
    await post('/api/sheets/$sheetUuid/annotations', {
      'page': page,
      'lastModified': lastModified.toIso8601String(),
      'annotations': annotationsJson,
    });
    _log.info('Annotation uploaded for sheet $sheetUuid page $page');
  }

  // ---------------------------------------------------------------------------
  // Change Sync Operations
  // ---------------------------------------------------------------------------

  /// Uploads a batch of changes to the server.
  ///
  /// The server will apply changes based on their [createdAt] timestamps,
  /// using the newest change for each field when resolving conflicts.
  ///
  /// Returns the list of change indices that were successfully applied.
  /// Throws [ApiException] if the request fails (e.g., offline).
  Future<List<int>> uploadChanges(List<Change> changes) async {
    if (changes.isEmpty) return [];

    final response = await post('/api/changes/sync', {
      'changes': changes.map((c) => c.toJson()).toList(),
    });

    final data = jsonDecode(response.body);
    final applied = (data['applied'] as List<dynamic>).cast<int>();
    _log.info('Uploaded ${changes.length} changes, ${applied.length} applied');
    return applied;
  }

  /// Checks if the server is reachable.
  ///
  /// Returns true if the server responds, false otherwise.
  Future<bool> checkConnection() async {
    try {
      final response = await get(
        "/api/health",
      ).timeout(const Duration(seconds: 5));
      return response.statusCode == 200;
    } catch (e) {
      _log.fine('Connection check failed: $e');
      return false;
    }
  }
}

/// Represents an annotation from the server.
class ServerAnnotation {
  final int page;
  final DateTime lastModified;
  final String annotationsJson;

  ServerAnnotation({
    required this.page,
    required this.lastModified,
    required this.annotationsJson,
  });

  factory ServerAnnotation.fromJson(Map<String, dynamic> json) {
    return ServerAnnotation(
      page: json['page'] as int,
      lastModified: DateTime.parse(json['lastModified'] as String),
      annotationsJson: json['annotations'] as String,
    );
  }
}

/// Exception thrown when an API request fails.
class ApiException implements Exception {
  final String message;
  final int statusCode;
  final String body;

  ApiException(this.message, {required this.statusCode, required this.body});

  @override
  String toString() => 'ApiException: $message (status: $statusCode)\n$body';
}
