import 'dart:math' as math;

import 'package:flutter/material.dart';
// skeleton rendering is controlled by the caller's `Skeletonizer` wrapper
// so this widget doesn't import `skeletonizer` directly.

import '../theme/app_typography.dart';
import '../theme/app_surfaces.dart';
import 'mono_text.dart';

/// A column configuration for the [TasQAdaptiveList] desktop table view.
class TasQColumn<T> {
  /// Creates a column configuration.
  const TasQColumn({
    required this.header,
    required this.cellBuilder,
    this.technical = false,
  });

  /// The column header text.
  final String header;

  /// Builds the cell content for each row.
  final Widget Function(BuildContext context, T item) cellBuilder;

  /// If true, applies monospace text style to the cell content.
  final bool technical;
}

/// Builds a mobile tile for [TasQAdaptiveList].
typedef TasQMobileTileBuilder<T> =
    Widget Function(BuildContext context, T item, List<Widget> actions);

/// Returns a list of action widgets for a given item.
typedef TasQRowActions<T> = List<Widget> Function(T item);

/// Callback when a row is tapped.
typedef TasQRowTap<T> = void Function(T item);

/// A adaptive list widget that renders as:
/// - **Mobile**: Tile-based list with infinite scroll listeners.
/// - **Desktop**: Data Table with paginated footer.
///
/// The widget requires a reactive data source ([items]) that responds to
/// pagination/search providers for server-side data fetching.
class TasQAdaptiveList<T> extends StatelessWidget {
  /// Creates an adaptive list.
  const TasQAdaptiveList({
    super.key,
    required this.items,
    required this.columns,
    required this.mobileTileBuilder,
    this.rowActions,
    this.onRowTap,
    this.rowsPerPage = 50,
    this.totalCount,
    this.pageOffset = 0,
    this.tableHeader,
    this.filterHeader,
    this.summaryDashboard,
    this.onRequestRefresh,
    this.onPageChanged,
    this.isLoading = false,
    this.skeletonMode = false,
  });

  /// The list of items to display.
  final List<T> items;

  /// The column configurations for the desktop table view.
  final List<TasQColumn<T>> columns;

  /// Builds the mobile tile for each item.
  final TasQMobileTileBuilder<T> mobileTileBuilder;

  /// Returns action widgets for each row (e.g., edit/delete buttons).
  final TasQRowActions<T>? rowActions;

  /// Callback when a row is tapped.
  final TasQRowTap<T>? onRowTap;

  /// Number of rows per page for desktop view.
  ///
  /// Per CLAUDE.md: Standard page size is 50 items for Desktop.
  final int rowsPerPage;

  /// Optional total number of rows in the full result set. When non-null
  /// and [items] contains only the current page, this value will be used
  /// as the `rowCount` for the table so pagination controls render correctly.
  final int? totalCount;

  /// Offset of the first item in [items] relative to the full result set.
  /// Used when [totalCount] is provided and [items] represents a page.
  final int pageOffset;

  /// Optional header widget for the desktop table.
  final Widget? tableHeader;

  /// Optional filter header widget that appears above the list/table.
  final Widget? filterHeader;

  /// Optional summary dashboard widget (e.g., status counts).
  final Widget? summaryDashboard;

  /// Callback when the user requests refresh (infinite scroll or pagination.
  ///
  /// * **Mobile** – invoked when the user scrolls to the end of the list.
  /// * **Desktop** – *not* triggered by page changes (see [onPageChanged]).
  final void Function()? onRequestRefresh;

  /// Desktop-only callback invoked when the paginated table changes page.
  ///
  /// The integer parameter is the first row index for the newly visible
  /// page, which corresponds directly to the `offset` value used by our
  /// server-side pagination providers. Consumers should update the
  /// associated query provider (e.g. `tasksQueryProvider`) so that the
  /// underlying stream/future is refreshed with the new range.
  final void Function(int firstRowIndex)? onPageChanged;

  /// If true, shows a loading indicator for server-side pagination.
  final bool isLoading;

  /// When true the widget renders skeleton placeholders for the
  /// dashboard, filter panel and list items instead of the real content.
  /// This is intended to provide a single-source skeleton UI for screens
  /// that wrap the whole body in a `Skeletonizer` and want consistent
  /// sectioned placeholders.
  final bool skeletonMode;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final isMobile = constraints.maxWidth < 600;

        if (isMobile) {
          return _buildMobile(context, constraints);
        }

        return _buildDesktop(context, constraints);
      },
    );
  }

  Widget _buildMobile(BuildContext context, BoxConstraints constraints) {
    final hasBoundedHeight = constraints.hasBoundedHeight;

    if (skeletonMode) {
      // Render structured skeleton sections: summary, filters, and list.
      final summary = summaryDashboard == null
          ? const SizedBox.shrink()
          : Column(
              children: [
                SizedBox(width: double.infinity, child: summaryDashboard!),
                const SizedBox(height: 12),
              ],
            );
      final filter = filterHeader == null
          ? const SizedBox.shrink()
          : Column(
              children: [
                ExpansionTile(
                  title: const Text('Filters'),
                  children: [
                    Padding(
                      padding: const EdgeInsets.only(bottom: 8),
                      child: filterHeader!,
                    ),
                  ],
                ),
                const SizedBox(height: 8),
              ],
            );

      final skeletonList = ListView.separated(
        padding: const EdgeInsets.only(bottom: 24),
        itemCount: 6,
        separatorBuilder: (context, index) => const SizedBox(height: 12),
        itemBuilder: (context, index) => _loadingTile(context),
      );

      return Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            if (summaryDashboard != null) ...[summary],
            if (filterHeader != null) ...[filter],
            Expanded(child: _buildInfiniteScrollListener(skeletonList)),
          ],
        ),
      );
    }

    // Mobile: Single-column with infinite scroll listeners
    final listView = ListView.separated(
      padding: const EdgeInsets.only(bottom: 24),
      itemCount: items.length + (isLoading ? 1 : 0),
      separatorBuilder: (context, index) => const SizedBox(height: 12),
      itemBuilder: (context, index) {
        if (index >= items.length) {
          // Loading skeleton for infinite scroll (non-blocking shimmer)
          return _loadingTile(context);
        }
        final item = items[index];
        final actions = rowActions?.call(item) ?? const <Widget>[];
        return _MobileTile(
          item: item,
          actions: actions,
          mobileTileBuilder: mobileTileBuilder,
          onRowTap: onRowTap,
        );
      },
    );

    // Shrink-wrapped list for unbounded height contexts
    final shrinkWrappedList = ListView.separated(
      padding: const EdgeInsets.only(bottom: 24),
      itemCount: items.length + (isLoading ? 1 : 0),
      separatorBuilder: (context, index) => const SizedBox(height: 12),
      itemBuilder: (context, index) {
        if (index >= items.length) {
          return _loadingTile(context);
        }
        final item = items[index];
        final actions = rowActions?.call(item) ?? const <Widget>[];
        return _MobileTile(
          item: item,
          actions: actions,
          mobileTileBuilder: mobileTileBuilder,
          onRowTap: onRowTap,
        );
      },
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
    );

    final summarySection = summaryDashboard == null
        ? null
        : <Widget>[
            SizedBox(width: double.infinity, child: summaryDashboard!),
            const SizedBox(height: 12),
          ];
    final filterSection = filterHeader == null
        ? null
        : <Widget>[
            ExpansionTile(
              title: const Text('Filters'),
              children: [
                Padding(
                  padding: const EdgeInsets.only(bottom: 8),
                  child: filterHeader!,
                ),
              ],
            ),
            const SizedBox(height: 8),
          ];

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          ...?summarySection,
          ...?filterSection,
          if (hasBoundedHeight)
            Expanded(child: _buildInfiniteScrollListener(listView))
          else
            _buildInfiniteScrollListener(shrinkWrappedList),
        ],
      ),
    );
  }

  Widget _buildInfiniteScrollListener(Widget listView) {
    if (onRequestRefresh == null) {
      return listView;
    }
    return NotificationListener<ScrollNotification>(
      onNotification: (notification) {
        if (notification is ScrollEndNotification &&
            notification.metrics.extentAfter == 0) {
          // User scrolled to bottom, trigger load more
          onRequestRefresh!();
        }
        return false;
      },
      child: listView,
    );
  }

  Widget _loadingTile(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 8),
      child: SizedBox(
        height: 72,
        child: Card(
          margin: EdgeInsets.zero,
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            child: Row(
              children: [
                Container(
                  width: 40,
                  height: 40,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(6),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Container(height: 12, color: Colors.white),
                      const SizedBox(height: 8),
                      Container(height: 10, width: 150, color: Colors.white),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildDesktop(BuildContext context, BoxConstraints constraints) {
    // Use progressively smaller fractions of the viewport on larger screens
    // so that content doesn't stretch too widely, but also consume as much
    // width as possible when the display is more modest.
    double contentFactor;
    if (constraints.maxWidth < 1200) {
      contentFactor = 0.95; // not-so-wide monitors
    } else if (constraints.maxWidth < 1800) {
      contentFactor = 0.85; // wide monitors
    } else {
      contentFactor = 0.75; // ultra-wide monitors
    }
    final contentWidth = constraints.maxWidth * contentFactor;

    // Table width: mirrors PaginatedDataTable's own internal layout so the
    // horizontal scrollbar appears exactly when columns would otherwise be
    // squeezed below their minimum comfortable width.
    //   • horizontalMargin=16 → 32 px total side padding
    //   • columnSpacing=20 between each adjacent pair
    //   • 200 px minimum per data column, 140 px for the actions column
    const double colMinW = 200.0;
    const double actMinW = 140.0;
    const double hMarginTotal = 16.0 * 2;
    const double colSpacing = 20.0;
    final actionsColumnCount = rowActions == null ? 0 : 1;
    final totalCols = columns.length + actionsColumnCount;
    final minColumnsWidth =
        hMarginTotal +
        (columns.length * colMinW) +
        (actionsColumnCount * actMinW) +
        (math.max(0, totalCols - 1) * colSpacing);
    final tableWidth = math.max(contentWidth, minColumnsWidth);

    final summaryWidget = summaryDashboard == null
        ? null
        : Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              SizedBox(width: contentWidth, child: summaryDashboard!),
              const SizedBox(height: 12),
            ],
          );
    final filterWidget = filterHeader == null
        ? null
        : Column(
            mainAxisSize: MainAxisSize.min,
            children: [filterHeader!, const SizedBox(height: 12)],
          );

    return _DesktopTableView<T>(
      items: items,
      columns: columns,
      rowActions: rowActions,
      onRowTap: onRowTap,
      maxRowsPerPage: rowsPerPage,
      totalCount: totalCount,
      pageOffset: pageOffset,
      tableHeader: tableHeader,
      onPageChanged: onPageChanged,
      contentWidth: contentWidth,
      tableWidth: tableWidth,
      summaryWidget: summaryWidget,
      filterWidget: filterWidget,
      hasBoundedHeight: constraints.hasBoundedHeight,
    );
  }
}

/// Stateful desktop table view that properly handles:
/// - Auto-computed rows per page in bounded-height contexts (headers stay
///   visible because the table always fits without vertical overflow).
/// - Horizontal scrollbar only when table width exceeds viewport.
/// - Clean [ScrollController] lifecycle.
class _DesktopTableView<T> extends StatefulWidget {
  const _DesktopTableView({
    super.key,
    required this.items,
    required this.columns,
    required this.rowActions,
    required this.onRowTap,
    required this.maxRowsPerPage,
    required this.totalCount,
    required this.pageOffset,
    required this.tableHeader,
    required this.onPageChanged,
    required this.contentWidth,
    required this.tableWidth,
    required this.summaryWidget,
    required this.filterWidget,
    required this.hasBoundedHeight,
  });

  final List<T> items;
  final List<TasQColumn<T>> columns;
  final TasQRowActions<T>? rowActions;
  final TasQRowTap<T>? onRowTap;
  final int maxRowsPerPage;
  final int? totalCount;
  final int pageOffset;
  final Widget? tableHeader;
  final void Function(int)? onPageChanged;
  final double contentWidth;
  final double tableWidth;
  final Widget? summaryWidget;
  final Widget? filterWidget;
  final bool hasBoundedHeight;

  @override
  State<_DesktopTableView<T>> createState() => _DesktopTableViewState<T>();
}

class _DesktopTableViewState<T> extends State<_DesktopTableView<T>> {
  final ScrollController _horizontalController = ScrollController();

  @override
  void dispose() {
    _horizontalController.dispose();
    super.dispose();
  }

  /// Compute rows per page AND the per-row height from the exact pixel budget
  /// available for the table section, so the rendered table fills the space
  /// without leaving empty space below the pagination footer.
  ///
  /// Returns a `(rowsPerPage, rowHeight)` record.  [rowHeight] is ≥ 48 px
  /// (Flutter's `dataRowMinHeight` default) so rows never collapse.
  (int, double) _computeRowLayout(double availableHeight) {
    // PaginatedDataTable internal chrome heights (from Flutter source):
    //  Heading row   – 56 px
    //  Footer row    – 56 px
    //  Optional card header  – 64 px  (only when tableHeader != null)
    //  Card border/shadow    – ~4 px
    const double colHeaderH = 56.0;
    const double footerH = 56.0;
    const double defaultRowH = 48.0;
    const double cardPad = 4.0;
    final double headerH = widget.tableHeader != null ? 64.0 : 0.0;

    final overhead = colHeaderH + footerH + headerH + cardPad;
    final availableForRows = availableHeight - overhead;
    if (availableForRows <= 0) return (1, defaultRowH);

    // How many rows fit at minimum height?
    final maxFitting = math.max(1, (availableForRows / defaultRowH).floor());
    // Never show blank rows — cap to the number of items we actually have.
    final itemCount = math.max(1, widget.items.length);
    final rows = math.min(
      maxFitting,
      math.min(widget.maxRowsPerPage, itemCount),
    );

    // Expand each row to fill the leftover space so the table reaches
    // exactly the bottom of the Expanded widget — no empty space.
    final rowHeight = math.max(defaultRowH, availableForRows / rows);
    return (rows, rowHeight);
  }

  Widget _buildTable(
    BuildContext context,
    int rowsPerPage, {
    double? rowHeight,
  }) {
    final dataSource = _TasQTableSource<T>(
      context: context,
      items: widget.items,
      columns: widget.columns,
      rowActions: widget.rowActions,
      onRowTap: widget.onRowTap,
      offset: widget.pageOffset,
      totalCount: widget.totalCount,
    );

    final table = PaginatedDataTable(
      header: widget.tableHeader,
      rowsPerPage: rowsPerPage,
      columnSpacing: 20,
      horizontalMargin: 16,
      showCheckboxColumn: false,
      // When a rowHeight is provided (bounded layout), expand rows to fill
      // the exact available height so there is no empty space after paging.
      dataRowMinHeight: rowHeight ?? 48.0,
      dataRowMaxHeight: rowHeight ?? 48.0,
      headingRowColor: WidgetStateProperty.resolveWith(
        (states) => Theme.of(context).colorScheme.surfaceContainer,
      ),
      columns: [
        for (final column in widget.columns)
          DataColumn(label: Text(column.header)),
        if (widget.rowActions != null) const DataColumn(label: Text('Actions')),
      ],
      source: dataSource,
      onPageChanged: widget.onPageChanged,
    );

    // Only wrap in a horizontal scroll + scrollbar when columns genuinely
    // exceed the available width.
    if (widget.tableWidth > widget.contentWidth) {
      return Scrollbar(
        controller: _horizontalController,
        thumbVisibility: true,
        trackVisibility: true,
        child: SingleChildScrollView(
          controller: _horizontalController,
          primary: false,
          scrollDirection: Axis.horizontal,
          child: SizedBox(width: widget.tableWidth, child: table),
        ),
      );
    }

    return table;
  }

  @override
  Widget build(BuildContext context) {
    if (widget.hasBoundedHeight) {
      // Bounded: use Expanded around a LayoutBuilder so we know the *exact*
      // pixel budget for the table, then auto-compute rowsPerPage so the
      // table always fits vertically (→ headers never scroll away).
      return Center(
        child: SizedBox(
          key: const Key('adaptive_list_content'),
          width: widget.contentWidth,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              if (widget.summaryWidget != null) widget.summaryWidget!,
              if (widget.filterWidget != null) widget.filterWidget!,
              Expanded(
                child: LayoutBuilder(
                  builder: (context, tableConstraints) {
                    final (rows, rowH) = _computeRowLayout(
                      tableConstraints.maxHeight,
                    );
                    return _buildTable(context, rows, rowHeight: rowH);
                  },
                ),
              ),
            ],
          ),
        ),
      );
    }

    // Unbounded: use the requested rowsPerPage (capped to current page's
    // items.length to avoid blank-row padding at the bottom).
    final defaultRows = math.min(
      widget.maxRowsPerPage,
      math.max(1, widget.items.length),
    );
    return Center(
      child: SizedBox(
        key: const Key('adaptive_list_content'),
        width: widget.contentWidth,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            if (widget.summaryWidget != null) widget.summaryWidget!,
            if (widget.filterWidget != null) widget.filterWidget!,
            _buildTable(context, defaultRows),
          ],
        ),
      ),
    );
  }
}

/// Mobile tile wrapper that applies Material 2 style elevation.
class _MobileTile<T> extends StatelessWidget {
  const _MobileTile({
    required this.item,
    required this.actions,
    required this.mobileTileBuilder,
    required this.onRowTap,
  });

  final T item;
  final List<Widget> actions;
  final TasQMobileTileBuilder<T> mobileTileBuilder;
  final TasQRowTap<T>? onRowTap;

  @override
  Widget build(BuildContext context) {
    final tile = mobileTileBuilder(context, item, actions);

    // Apply Material 2 style elevation for Cards (per Hybrid M3/M2 guidelines).
    // Mobile tiles deliberately use a slightly smaller corner radius for
    // compactness, but they should inherit the global card elevation and
    // shadow color from the theme to maintain visual consistency.
    if (tile is Card) {
      final themeCard = Theme.of(context).cardTheme;
      return Card(
        color: tile.color,
        elevation: themeCard.elevation ?? 3,
        margin: tile.margin,
        // prefer the tile's explicit shape. For mobile tiles we intentionally
        // use the compact radius token so list items feel denser while
        // remaining theme-driven.
        shape: tile.shape ?? AppSurfaces.of(context).compactShape,
        shadowColor: AppSurfaces.of(context).compactShadowColor,
        clipBehavior: tile.clipBehavior,
        child: tile.child,
      );
    }

    return tile;
  }
}

class _TasQTableSource<T> extends DataTableSource {
  /// Creates a table source for [TasQAdaptiveList].
  _TasQTableSource({
    required this.context,
    required this.items,
    required this.columns,
    required this.rowActions,
    required this.onRowTap,
    this.offset = 0,
    this.totalCount,
  });

  final BuildContext context;
  final List<T> items;
  final List<TasQColumn<T>> columns;
  final TasQRowActions<T>? rowActions;
  final TasQRowTap<T>? onRowTap;
  final int offset;
  final int? totalCount;

  @override
  DataRow? getRow(int index) {
    // Map the global table index to the local items page using the
    // provided offset. If items contains the full dataset, offset will be
    // zero and this reduces to a direct index. If items contains only the
    // current page, index may be outside the local range — return null in
    // that case so the table can render blanks for non-loaded rows.
    final localIndex = index - offset;
    if (localIndex < 0 || localIndex >= items.length) return null;
    final item = items[localIndex];
    final cells = <DataCell>[];

    for (final column in columns) {
      final widget = column.cellBuilder(context, item);
      cells.add(DataCell(_applyTechnicalStyle(context, widget, column)));
    }

    if (rowActions != null) {
      final actions = rowActions!.call(item);
      cells.add(
        DataCell(Row(mainAxisSize: MainAxisSize.min, children: actions)),
      );
    }

    return DataRow(
      onSelectChanged: onRowTap == null ? null : (_) => onRowTap!(item),
      cells: cells,
    );
  }

  @override
  bool get isRowCountApproximate => false;

  @override
  int get rowCount => totalCount ?? items.length;

  @override
  int get selectedRowCount => 0;
}

Widget _applyTechnicalStyle<T>(
  BuildContext context,
  Widget child,
  TasQColumn<T> column,
) {
  if (!column.technical) return child;
  if (child is Text && child.data != null) {
    return MonoText(
      child.data ?? '',
      style: child.style,
      maxLines: child.maxLines,
      overflow: child.overflow,
      textAlign: child.textAlign,
    );
  }

  final mono = AppMonoText.of(context).body;
  return DefaultTextStyle.merge(style: mono, child: child);
}
