import 'package:flutter/foundation.dart';

import '../domain/product_draft.dart';
import '../domain/publish_status.dart';
import 'get_product_drafts.dart';
import 'publish_product.dart';
import 'update_product_category.dart';
import 'update_product_description.dart';
import 'update_product_name.dart';
import 'update_product_price.dart';
import 'update_product_status.dart';

/// The display density for the product list.
enum ListDensity {
  /// Standard card height with full metadata.
  standard,

  /// Compact card height with condensed metadata for faster scanning.
  compact,
}

/// The field used to sort the visible product list.
enum ProductSortField {
  /// Sort alphabetically by product name.
  name,

  /// Sort by last-modified date.
  lastModified,

  /// Sort by publishing status (draft → pendingReview → published → unpublished).
  status,
}

/// The outcome of a single status-change action (publish / move-to-draft).
///
/// Consumed once by the UI to show a SnackBar, then cleared.
class StatusActionResult {
  final bool success;
  final String productName;
  final PublishStatus targetStatus;
  final String? errorMessage;

  const StatusActionResult({
    required this.success,
    required this.productName,
    required this.targetStatus,
    this.errorMessage,
  });
}

/// The outcome of a price-update action.
///
/// Consumed once by the UI to show a SnackBar, then cleared.
class PriceActionResult {
  final bool success;
  final String productName;
  final double newPrice;
  final String? errorMessage;

  const PriceActionResult({
    required this.success,
    required this.productName,
    required this.newPrice,
    this.errorMessage,
  });
}

/// The outcome of a name-update action.
///
/// Consumed once by the UI to show a SnackBar, then cleared.
class NameActionResult {
  final bool success;
  final String productName;
  final String newName;
  final String? errorMessage;

  const NameActionResult({
    required this.success,
    required this.productName,
    required this.newName,
    this.errorMessage,
  });
}

/// The outcome of a description-update action.
///
/// Consumed once by the UI to show a SnackBar, then cleared.
class DescriptionActionResult {
  final bool success;
  final String productName;
  final String? errorMessage;

  const DescriptionActionResult({
    required this.success,
    required this.productName,
    this.errorMessage,
  });
}

/// The outcome of a category-update action.
///
/// Consumed once by the UI to show a SnackBar, then cleared.
class CategoryActionResult {
  final bool success;
  final String productName;
  final String newCategory;
  final String? errorMessage;

  const CategoryActionResult({
    required this.success,
    required this.productName,
    required this.newCategory,
    this.errorMessage,
  });
}

/// The outcome of a bulk status-change action (e.g. bulk move-to-draft).
///
/// Consumed once by the UI to show a SnackBar, then cleared.
class BulkActionResult {
  /// The number of products that were successfully updated.
  final int successCount;

  /// The number of products that failed to update.
  final int failureCount;

  /// The target status that was applied.
  final PublishStatus targetStatus;

  /// Names of products that failed, for operator visibility.
  final List<String> failedProductNames;

  const BulkActionResult({
    required this.successCount,
    required this.failureCount,
    required this.targetStatus,
    this.failedProductNames = const [],
  });

  /// The total number of products that were attempted.
  int get totalCount => successCount + failureCount;

  /// Whether all attempted products were successfully updated.
  bool get allSucceeded => failureCount == 0;
}

/// Controller that manages the product publishing workspace state, including
/// filtering by publish status, free-text search, and draft selection.
class ProductPublishingController extends ChangeNotifier {
  final GetProductDrafts _getProductDrafts;
  final PublishProduct _publishProduct;
  final UpdateProductStatus _updateProductStatus;
  final UpdateProductPrice _updateProductPrice;
  final UpdateProductName _updateProductName;
  final UpdateProductDescription _updateProductDescription;
  final UpdateProductCategory _updateProductCategory;

  ProductPublishingController(
    this._getProductDrafts,
    this._publishProduct,
    this._updateProductStatus,
    this._updateProductPrice,
    this._updateProductName,
    this._updateProductDescription,
    this._updateProductCategory,
  );

  bool _disposed = false;

  bool isLoading = false;
  Object? error;

  /// All drafts returned by the repository (unfiltered).
  List<ProductDraft> _allDrafts = [];

  /// The currently visible drafts after applying [activeFilter] and [searchQuery].
  List<ProductDraft> drafts = [];

  /// The currently selected draft for preview.
  ProductDraft? selectedDraft;

  /// The active status filter label, or `null` for "all".
  ///
  /// Recognised values: `'draft'`, `'pendingReview'`, `'published'`, `'unpublished'`.
  String? activeFilter;

  /// The current free-text search query applied to name, SKU, and category.
  ///
  /// Multi-word queries use AND semantics: every whitespace-separated token
  /// must appear in at least one of the searchable fields.
  String searchQuery = '';

  /// The field used to sort the visible product list.
  ProductSortField activeSortField = ProductSortField.name;

  /// Whether the current sort is ascending (`true`) or descending (`false`).
  bool sortAscending = true;

  /// The current display density for the product list.
  ListDensity listDensity = ListDensity.standard;

  /// The number of days after which a product is considered stale.
  ///
  /// Products whose [ProductDraft.lastModified] is older than this many days
  /// from [DateTime.now] are flagged with a visual indicator.
  static const int staleDays = 30;

  /// Product IDs that the operator has selected for potential bulk actions.
  ///
  /// Multi-select is independent of the single-item [selectedDraft] preview.
  /// Adding or removing items here does not change which product is shown
  /// in the detail panel.
  final Set<String> multiSelectedIds = {};

  /// The number of products currently multi-selected.
  int get multiSelectedCount => multiSelectedIds.length;

  /// Whether multi-select mode is active (at least one item selected).
  bool get isMultiSelectActive => multiSelectedIds.isNotEmpty;

  /// Whether the product with [id] is part of the multi-selection.
  bool isMultiSelected(String id) => multiSelectedIds.contains(id);

  /// Toggles the multi-select state of the product with [id].
  ///
  /// If the product is already selected it is removed; otherwise it is added.
  void toggleMultiSelect(String id) {
    if (multiSelectedIds.contains(id)) {
      multiSelectedIds.remove(id);
    } else {
      multiSelectedIds.add(id);
    }
    _safeNotify();
  }

  /// Selects all currently visible products (those in [drafts]).
  void selectAllVisible() {
    multiSelectedIds.addAll(drafts.map((d) => d.id));
    _safeNotify();
  }

  /// Clears the entire multi-selection.
  void clearMultiSelection() {
    multiSelectedIds.clear();
    _safeNotify();
  }

  /// Toggles the list density between [ListDensity.standard] and
  /// [ListDensity.compact].
  void toggleListDensity() {
    listDensity = listDensity == ListDensity.standard ? ListDensity.compact : ListDensity.standard;
    _safeNotify();
  }

  /// Whether the given [draft] is stale (not modified within [staleDays]).
  ///
  /// Accepts an optional [now] parameter for testability; defaults to
  /// [DateTime.now].
  bool isStale(ProductDraft draft, {DateTime? now}) {
    final reference = now ?? DateTime.now();
    return reference.difference(draft.lastModified).inDays >= staleDays;
  }

  /// The number of currently visible products that are stale.
  ///
  /// Accepts an optional [now] parameter for testability.
  int staleCount({DateTime? now}) {
    final reference = now ?? DateTime.now();
    return drafts.where((d) => isStale(d, now: reference)).length;
  }

  /// Selects the next draft in the visible list relative to the current
  /// [selectedDraft]. Wraps to the first item at the end of the list.
  ///
  /// Returns `true` if the selection changed.
  bool selectNextDraft() {
    if (drafts.isEmpty) return false;
    if (selectedDraft == null) {
      selectedDraft = drafts.first;
      _safeNotify();
      return true;
    }
    final currentIndex = drafts.indexWhere((d) => d.id == selectedDraft!.id);
    final nextIndex = (currentIndex + 1) % drafts.length;
    selectedDraft = drafts[nextIndex];
    _safeNotify();
    return true;
  }

  /// Selects the previous draft in the visible list relative to the current
  /// [selectedDraft]. Wraps to the last item at the beginning of the list.
  ///
  /// Returns `true` if the selection changed.
  bool selectPreviousDraft() {
    if (drafts.isEmpty) return false;
    if (selectedDraft == null) {
      selectedDraft = drafts.last;
      _safeNotify();
      return true;
    }
    final currentIndex = drafts.indexWhere((d) => d.id == selectedDraft!.id);
    final prevIndex = (currentIndex - 1 + drafts.length) % drafts.length;
    selectedDraft = drafts[prevIndex];
    _safeNotify();
    return true;
  }

  /// Product IDs that currently have an in-flight status update.
  ///
  /// Used to prevent duplicate clicks and to let the UI show per-row
  /// progress indicators without locking the entire page.
  final Set<String> updatingIds = {};

  /// Whether the product with [id] is currently being updated.
  bool isUpdating(String id) => updatingIds.contains(id);

  /// The result of the last status-change action.
  ///
  /// Set after [publish] or [updateStatus] completes. The UI should read
  /// this once to show feedback (e.g. a SnackBar) and then call
  /// [consumeActionResult] to clear it.
  StatusActionResult? lastActionResult;

  /// The result of the last price-update action.
  ///
  /// Set after [updatePrice] completes. The UI should read this once to
  /// show feedback (e.g. a SnackBar) and then call [consumePriceResult]
  /// to clear it.
  PriceActionResult? lastPriceResult;

  /// The result of the last name-update action.
  ///
  /// Set after [updateName] completes. The UI should read this once to
  /// show feedback (e.g. a SnackBar) and then call [consumeNameResult]
  /// to clear it.
  NameActionResult? lastNameResult;

  /// The result of the last description-update action.
  ///
  /// Set after [updateDescription] completes. The UI should read this once to
  /// show feedback (e.g. a SnackBar) and then call [consumeDescriptionResult]
  /// to clear it.
  DescriptionActionResult? lastDescriptionResult;

  /// The result of the last category-update action.
  ///
  /// Set after [updateCategory] completes. The UI should read this once to
  /// show feedback (e.g. a SnackBar) and then call [consumeCategoryResult]
  /// to clear it.
  CategoryActionResult? lastCategoryResult;

  /// Clears [lastActionResult] so the same result is not shown twice.
  void consumeActionResult() {
    lastActionResult = null;
  }

  /// Clears [lastPriceResult] so the same result is not shown twice.
  void consumePriceResult() {
    lastPriceResult = null;
  }

  /// Clears [lastNameResult] so the same result is not shown twice.
  void consumeNameResult() {
    lastNameResult = null;
  }

  /// Clears [lastDescriptionResult] so the same result is not shown twice.
  void consumeDescriptionResult() {
    lastDescriptionResult = null;
  }

  /// Clears [lastCategoryResult] so the same result is not shown twice.
  void consumeCategoryResult() {
    lastCategoryResult = null;
  }

  /// The result of the last bulk status-change action.
  ///
  /// Set after [bulkUpdateStatus] completes. The UI should read this once
  /// to show feedback (e.g. a SnackBar) and then call
  /// [consumeBulkActionResult] to clear it.
  BulkActionResult? lastBulkActionResult;

  /// Clears [lastBulkActionResult] so the same result is not shown twice.
  void consumeBulkActionResult() {
    lastBulkActionResult = null;
  }

  /// Updates the publishing status of all [multiSelectedIds] to [status].
  ///
  /// Processes each selected product sequentially to respect API rate limits.
  /// Tracks per-row updating state via [updatingIds]. After all updates
  /// complete (whether successful or not), reloads once and clears the
  /// multi-selection.
  ///
  /// Does nothing if no items are multi-selected.
  Future<void> bulkUpdateStatus(PublishStatus status) async {
    if (multiSelectedIds.isEmpty) return;

    // Snapshot the ids to process — the set may be cleared during iteration.
    final idsToProcess = List<String>.of(multiSelectedIds);

    // Mark all as updating.
    updatingIds.addAll(idsToProcess);
    _safeNotify();

    int successCount = 0;
    int failureCount = 0;
    final failedNames = <String>[];

    for (final id in idsToProcess) {
      if (_disposed) return;
      try {
        await _updateProductStatus(id, status);
        successCount++;
      } catch (_) {
        failureCount++;
        failedNames.add(_productNameById(id));
      }
    }

    if (_disposed) return;

    // Clean up updating state and multi-selection.
    updatingIds.removeAll(idsToProcess);
    multiSelectedIds.clear();

    lastBulkActionResult = BulkActionResult(
      successCount: successCount,
      failureCount: failureCount,
      targetStatus: status,
      failedProductNames: failedNames,
    );

    // Single reload after all updates.
    await load();
  }

  /// Loads all product drafts and applies any current filter / search.
  Future<void> load() async {
    isLoading = true;
    error = null;
    _safeNotify();

    try {
      _allDrafts = await _getProductDrafts();
      if (_disposed) return;
      _applyFilters();
      // Auto-select the first draft if nothing is selected.
      selectedDraft ??= drafts.isNotEmpty ? drafts.first : null;
    } catch (e) {
      if (_disposed) return;
      error = e;
    } finally {
      isLoading = false;
      _safeNotify();
    }
  }

  /// Sets the status filter and recomputes the visible list.
  void setFilter(String? filter) {
    activeFilter = filter;
    _applyFilters();
    _safeNotify();
  }

  /// Sets the search query and recomputes the visible list.
  void setSearchQuery(String query) {
    searchQuery = query;
    _applyFilters();
    _safeNotify();
  }

  /// Sets the sort field and direction, then recomputes the visible list.
  void setSort(ProductSortField field, {bool? ascending}) {
    activeSortField = field;
    if (ascending != null) {
      sortAscending = ascending;
    }
    _applyFilters();
    _safeNotify();
  }

  /// Selects a draft for preview.
  void selectDraft(ProductDraft draft) {
    selectedDraft = draft;
    _safeNotify();
  }

  /// Attempts to select a draft by SKU. Returns `true` if found.
  bool selectBySku(String sku) {
    final match = _allDrafts.where((d) => d.sku == sku).firstOrNull;
    if (match != null) {
      selectedDraft = match;
      _safeNotify();
      return true;
    }
    return false;
  }

  /// Publishes the draft with the given [id] and reloads the list.
  Future<void> publish(String id) async {
    // Prevent duplicate clicks while this row is already updating.
    if (updatingIds.contains(id)) return;

    final productName = _productNameById(id);

    updatingIds.add(id);
    _safeNotify();

    try {
      await _publishProduct(id);
      if (_disposed) return;
      updatingIds.remove(id);
      lastActionResult = StatusActionResult(
        success: true,
        productName: productName,
        targetStatus: PublishStatus.published,
      );
      await load();
    } catch (e) {
      if (_disposed) return;
      updatingIds.remove(id);
      lastActionResult = StatusActionResult(
        success: false,
        productName: productName,
        targetStatus: PublishStatus.published,
        errorMessage: e.toString(),
      );
      _safeNotify();
    }
  }

  /// Updates the publishing status of the product with [id].
  ///
  /// Tracks per-row updating state via [updatingIds] so the UI can show
  /// a spinner on the affected row without locking the entire page.
  /// Silently ignores duplicate calls while [id] is already in flight.
  Future<void> updateStatus(String id, PublishStatus status) async {
    // Prevent duplicate clicks while this row is already updating.
    if (updatingIds.contains(id)) return;

    final productName = _productNameById(id);

    updatingIds.add(id);
    _safeNotify();

    try {
      await _updateProductStatus(id, status);
      if (_disposed) return;
      updatingIds.remove(id);
      lastActionResult = StatusActionResult(
        success: true,
        productName: productName,
        targetStatus: status,
      );
      await load();
    } catch (e) {
      if (_disposed) return;
      updatingIds.remove(id);
      lastActionResult = StatusActionResult(
        success: false,
        productName: productName,
        targetStatus: status,
        errorMessage: e.toString(),
      );
      _safeNotify();
    }
  }

  /// Updates only the price of the product with [id].
  ///
  /// Follows the same per-row updating pattern as [updateStatus].
  Future<void> updatePrice(String id, double price) async {
    if (updatingIds.contains(id)) return;

    final productName = _productNameById(id);

    updatingIds.add(id);
    _safeNotify();

    try {
      await _updateProductPrice(id, price);
      if (_disposed) return;
      updatingIds.remove(id);
      lastPriceResult = PriceActionResult(success: true, productName: productName, newPrice: price);
      await load();
    } catch (e) {
      if (_disposed) return;
      updatingIds.remove(id);
      lastPriceResult = PriceActionResult(
        success: false,
        productName: productName,
        newPrice: price,
        errorMessage: e.toString(),
      );
      _safeNotify();
    }
  }

  /// Updates only the name of the product with [id].
  ///
  /// Follows the same per-row updating pattern as [updateStatus].
  Future<void> updateName(String id, String name) async {
    if (updatingIds.contains(id)) return;

    final productName = _productNameById(id);

    updatingIds.add(id);
    _safeNotify();

    try {
      await _updateProductName(id, name);
      if (_disposed) return;
      updatingIds.remove(id);
      lastNameResult = NameActionResult(success: true, productName: productName, newName: name);
      await load();
    } catch (e) {
      if (_disposed) return;
      updatingIds.remove(id);
      lastNameResult = NameActionResult(
        success: false,
        productName: productName,
        newName: name,
        errorMessage: e.toString(),
      );
      _safeNotify();
    }
  }

  /// Updates only the description of the product with [id].
  ///
  /// Follows the same per-row updating pattern as [updateStatus].
  Future<void> updateDescription(String id, String description) async {
    if (updatingIds.contains(id)) return;

    final productName = _productNameById(id);

    updatingIds.add(id);
    _safeNotify();

    try {
      await _updateProductDescription(id, description);
      if (_disposed) return;
      updatingIds.remove(id);
      lastDescriptionResult = DescriptionActionResult(success: true, productName: productName);
      await load();
    } catch (e) {
      if (_disposed) return;
      updatingIds.remove(id);
      lastDescriptionResult = DescriptionActionResult(
        success: false,
        productName: productName,
        errorMessage: e.toString(),
      );
      _safeNotify();
    }
  }

  /// Updates only the category of the product with [id].
  ///
  /// Follows the same per-row updating pattern as [updateStatus].
  Future<void> updateCategory(String id, String category) async {
    if (updatingIds.contains(id)) return;

    final productName = _productNameById(id);

    updatingIds.add(id);
    _safeNotify();

    try {
      await _updateProductCategory(id, category);
      if (_disposed) return;
      updatingIds.remove(id);
      lastCategoryResult = CategoryActionResult(
        success: true,
        productName: productName,
        newCategory: category,
      );
      await load();
    } catch (e) {
      if (_disposed) return;
      updatingIds.remove(id);
      lastCategoryResult = CategoryActionResult(
        success: false,
        productName: productName,
        newCategory: category,
        errorMessage: e.toString(),
      );
      _safeNotify();
    }
  }

  // ── Lifecycle ───────────────────────────────────────────────────────────

  @override
  void dispose() {
    if (_disposed) return;
    _disposed = true;
    super.dispose();
  }

  // ── Private helpers ────────────────────────────────────────────────────

  /// Calls [notifyListeners] only if the controller has not been disposed.
  void _safeNotify() {
    if (!_disposed) notifyListeners();
  }

  /// Returns the product name for [id], or a fallback if not found.
  String _productNameById(String id) {
    return _allDrafts.where((d) => d.id == id).map((d) => d.name).firstOrNull ?? 'Product $id';
  }

  void _applyFilters() {
    var result = _allDrafts;

    // Status filter
    final status = _parseStatus(activeFilter);
    if (status != null) {
      result = result.where((d) => d.status == status).toList();
    }

    // Free-text search on name, SKU, and category.
    // Multi-word queries use AND semantics: every whitespace-separated
    // token must appear in at least one of the searchable fields.
    if (searchQuery.isNotEmpty) {
      final tokens = searchQuery
          .toLowerCase()
          .split(RegExp(r'\s+'))
          .where((t) => t.isNotEmpty)
          .toList();
      if (tokens.isNotEmpty) {
        result = result.where((d) {
          final haystack =
              '${d.name.toLowerCase()} ${d.sku.toLowerCase()} ${d.category.toLowerCase()}';
          return tokens.every((token) => haystack.contains(token));
        }).toList();
      }
    }

    // Sort
    result = _sortDrafts(result);

    drafts = result;

    // Keep selection valid and refresh to latest values.
    if (selectedDraft != null) {
      final refreshed = drafts.where((d) => d.id == selectedDraft!.id).firstOrNull;
      selectedDraft = refreshed;
    }
  }

  /// Sorts [items] according to [activeSortField] and [sortAscending].
  ///
  /// Uses name as a stable secondary sort when the primary field has ties.
  List<ProductDraft> _sortDrafts(List<ProductDraft> items) {
    if (items.length <= 1) return items;

    final sorted = List<ProductDraft>.of(items);
    final dir = sortAscending ? 1 : -1;

    sorted.sort((a, b) {
      int cmp;
      switch (activeSortField) {
        case ProductSortField.name:
          cmp = a.name.toLowerCase().compareTo(b.name.toLowerCase());
        case ProductSortField.lastModified:
          cmp = a.lastModified.compareTo(b.lastModified);
        case ProductSortField.status:
          cmp = a.status.index.compareTo(b.status.index);
      }
      // Stable secondary sort by name when primary field ties.
      if (cmp == 0 && activeSortField != ProductSortField.name) {
        cmp = a.name.toLowerCase().compareTo(b.name.toLowerCase());
      }
      return cmp * dir;
    });

    return sorted;
  }

  static PublishStatus? _parseStatus(String? filter) {
    if (filter == null) return null;
    switch (filter) {
      case 'draft':
        return PublishStatus.draft;
      case 'pendingReview':
        return PublishStatus.pendingReview;
      case 'published':
        return PublishStatus.published;
      case 'unpublished':
        return PublishStatus.unpublished;
      default:
        return null;
    }
  }
}
