import "package:flutter/material.dart";

import "../i18n.dart";
import "../main.dart";
import "../models.dart";
import "../service_parts.dart";
import "../theme.dart";

/// Every tab a car's page can show, in the order they appear when the car has no
/// arrangement of its own. Information sits second because the connected
/// service, when there is one, is what you came to look at.
///
/// Mirrors ALL_TAB_KEYS in the web CarDetail and arrangeableCarTabs in the API's
/// cars.go — the server rejects any key outside that set.
const List<String> kCarTabKeys = [
  "provider",
  "info",
  "services",
  "technical",
  "maintenance",
  "fuel",
  "charging",
  "documents",
  "parts",
  "reminders",
];

/// The tabs that can be switched off. "info" is deliberately absent: it is the
/// car itself, and a page with no tabs left would be a dead end. It can still be
/// moved, which is why the two lists are separate.
const List<String> kHideableCarTabKeys = [
  "provider",
  "services",
  "technical",
  "maintenance",
  "fuel",
  "charging",
  "documents",
  "parts",
  "reminders",
];

/// The Information rows, in their default order. Keys mirror hideableCarFields
/// in the API's cars.go.
const List<String> kCarInfoFieldKeys = [
  "oilSpec",
  "transmissionOil",
  "differentialOil",
  "brakeFluid",
  "coolant",
  "odometer",
  "serviceInterval",
  "nextDue",
  "registrationPlate",
  "registrationCountry",
  "vin",
  "fuelType",
  "buildDate",
  "firstRegistration",
];

/// The Service history columns, in their default order. Keys mirror
/// arrangeableServiceColumns in the API's cars.go, and the same list in the web
/// CarDetail — the server rejects anything else. "date" is here because it can
/// be moved, and absent from [kHideableServiceColumnKeys] because it cannot be
/// switched off.
///
/// Every part a service can have changed shares the one "parts" column. They are
/// a growing list, and a column apiece would widen the web's table without end;
/// the phone shows them as chips, but the key set has to be the same one the
/// server validates against.
const List<String> kServiceColumnKeys = [
  "date",
  "km",
  "nextDate",
  "nextKm",
  "parts",
  "notes",
  "file",
];

/// The columns that can be switched off — all but the date. Derived rather than
/// written out again, the way the API derives hideableServiceColumns' companion,
/// so the two cannot drift as columns are added.
final List<String> kHideableServiceColumnKeys =
    kServiceColumnKeys.where((key) => key != "date").toList();

/// The heading a column carries. The labels were translated as car.services.col*
/// long before they became keys, so the two are mapped rather than derived —
/// renaming a dozen strings in three languages to save this table would be the
/// wrong trade. The parts column borrows the form's heading on purpose: the
/// column and the form's section are the same thing.
const Map<String, String> _kServiceColumnLabels = {
  "date": "car.services.colDate",
  "km": "car.services.colKm",
  "nextDate": "car.services.colNextDate",
  "nextKm": "car.services.colNextKm",
  "parts": "forms.service.changedParts",
  "notes": "car.services.colNotes",
  "file": "car.services.colFile",
};

String serviceColumnLabel(String key) => t(_kServiceColumnLabels[key] ?? key);

/// The columns that are a value on a line, as against the ones that need a
/// block to themselves: what a service changed, what was written about it, and
/// what was filed with it.
const Set<String> kInlineServiceColumns = {"date", "km", "nextDate", "nextKm"};

/// A card's columns, grouped into the runs it lays them out in.
///
/// A card is not a table: the web app gives every column a cell of its own on
/// one line, which a phone has no width for. Consecutive short columns instead
/// share a wrapping line — a wrap flows left to right and then down, so the
/// arrangement survives intact — and each of the three that need room breaks
/// onto its own. Which means the grouping follows the car's order rather than
/// the catalogue's: move Notes between Km and Next date and it splits the short
/// columns into two runs, because that is what the arrangement asked for.
List<List<String>> serviceColumnRuns(List<String> columns) {
  final runs = <List<String>>[];
  for (final key in columns) {
    final inline = kInlineServiceColumns.contains(key);
    if (inline && runs.isNotEmpty && kInlineServiceColumns.contains(runs.last.last)) {
      runs.last.add(key);
    } else {
      runs.add([key]);
    }
  }
  return runs;
}

/// Applies a car's stored arrangement to a catalogue of keys.
///
/// The stored order may be partial and may name keys this release does not know:
/// anything unrecognised is dropped, and any key it leaves out follows the ones
/// it names. That is what puts a tab or row added in a later release at the end
/// of somebody's page rather than in the middle of it.
List<String> arrangeKeys(List<String> catalogue, List<String> order) {
  final arranged = <String>[];
  for (final key in order) {
    if (catalogue.contains(key) && !arranged.contains(key)) arranged.add(key);
  }
  arranged.addAll(catalogue.where((k) => !arranged.contains(k)));
  return arranged;
}

/// What this car's page shows: which tabs and Information rows are on, and the
/// order of each. Mirrors the web's view picker, except that the web rearranges
/// by dragging the tab bar and the rows themselves — a gesture the tab bar owns
/// on a touch screen — so both arrangements are made here instead, with a handle.
///
/// Edited as a draft and saved in one write rather than on every checkbox:
/// switching several off one at a time would make the page rearrange under the
/// finger between taps. Pops the updated [Car] when it saved.
class CarViewSheet extends StatefulWidget {
  final Car car;

  /// Whether this car could show a connected-service tab at all. Offering to
  /// hide a tab nobody can see would just be confusing.
  final bool showProvider;

  /// The provider's display name ("MyToyota"), used for that tab's label.
  final String providerLabel;

  const CarViewSheet({
    super.key,
    required this.car,
    required this.showProvider,
    this.providerLabel = "",
  });

  @override
  State<CarViewSheet> createState() => _CarViewSheetState();
}

class _CarViewSheetState extends State<CarViewSheet> {
  late List<String> _tabOrder;
  late List<String> _fieldOrder;
  late List<String> _columnOrder;
  late Set<String> _visibleTabs;
  late Set<String> _visibleFields;
  late Set<String> _visibleColumns;
  late Set<String> _visibleServiceParts;
  bool _saving = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    final car = widget.car;
    _tabOrder = arrangeKeys(kCarTabKeys, car.tabOrder);
    _fieldOrder = arrangeKeys(kCarInfoFieldKeys, car.fieldOrder);
    _visibleTabs = kHideableCarTabKeys.where((k) => !car.hiddenTabs.contains(k)).toSet();
    _visibleFields = kCarInfoFieldKeys.where((k) => !car.hiddenFields.contains(k)).toSet();
    _columnOrder = arrangeKeys(kServiceColumnKeys, car.serviceColumnOrder);
    _visibleColumns = kHideableServiceColumnKeys
        .where((k) => !car.hiddenServiceColumns.contains(k))
        .toSet();
    _visibleServiceParts = visibleParts(car).map((p) => p.key).toSet();
  }

  /// Whether [key] is one of the rows on offer below: Information never is
  /// (it cannot be switched off), and the connected service only when this car
  /// could show one.
  bool _isRow(String key) =>
      key != "info" && (key != "provider" || widget.showProvider);

  /// The tab rows on offer, in the car's arrangement.
  List<String> get _tabRows => _tabOrder.where(_isRow).toList();

  /// The connected-service row is named after the service once the car is
  /// linked to one ("MyToyota"); before that there is no name to use, so it
  /// falls back to the generic label rather than to car.tabs.provider, which is
  /// deliberately not a key — the web app labels that tab the same way.
  String _tabLabel(String key) {
    if (key != "provider") return t("car.tabs.$key");
    return widget.providerLabel.isEmpty ? t("car.tabs.connected") : widget.providerLabel;
  }

  /// Applies a drag over the rows to the full arrangement, which holds keys the
  /// rows do not offer. The movable slots are refilled from the new row
  /// sequence and everything else keeps the position it already had, so
  /// rearranging the rows can never move Information — or a connected-service
  /// tab this car cannot show — out from under the user.
  void _moveTab(int oldIndex, int newIndex) {
    final rows = _tabRows;
    rows.insert(newIndex, rows.removeAt(oldIndex));
    setState(() {
      var next = 0;
      _tabOrder = [
        for (final key in _tabOrder) _isRow(key) ? rows[next++] : key,
      ];
    });
  }

  void _moveField(int oldIndex, int newIndex) {
    setState(() => _fieldOrder.insert(newIndex, _fieldOrder.removeAt(oldIndex)));
  }

  void _moveColumn(int oldIndex, int newIndex) {
    setState(() => _columnOrder.insert(newIndex, _columnOrder.removeAt(oldIndex)));
  }

  Future<void> _save() async {
    setState(() {
      _saving = true;
      _error = null;
    });
    try {
      final car = await apiClient.updateCarView(
        widget.car.id,
        hiddenTabs: kHideableCarTabKeys.where((k) => !_visibleTabs.contains(k)).toList(),
        hiddenFields: kCarInfoFieldKeys.where((k) => !_visibleFields.contains(k)).toList(),
        hiddenServiceColumns:
            kHideableServiceColumnKeys.where((k) => !_visibleColumns.contains(k)).toList(),
        tabOrder: _tabOrder,
        fieldOrder: _fieldOrder,
        serviceColumnOrder: _columnOrder,
        hiddenServiceParts: kServiceParts
            .where((p) => !_visibleServiceParts.contains(p.key))
            .map((p) => p.key)
            .toList(),
      );
      if (mounted) Navigator.pop(context, car);
    } catch (e) {
      if (mounted) setState(() => _error = e.toString());
    } finally {
      if (mounted) setState(() => _saving = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final inset = MediaQuery.of(context).viewInsets.bottom;
    return Padding(
      padding: EdgeInsets.only(bottom: inset),
      child: DraggableScrollableSheet(
        expand: false,
        initialChildSize: 0.85,
        maxChildSize: 0.95,
        builder: (context, scrollController) => Column(
          children: [
            Padding(
              padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
              child: Row(
                children: [
                  Expanded(
                    child: Text(t("car.viewPicker.title"),
                        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
                  ),
                  IconButton(
                    icon: const Icon(Icons.close),
                    onPressed: () => Navigator.pop(context),
                  ),
                ],
              ),
            ),
            Expanded(
              child: ListView(
                controller: scrollController,
                padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
                children: [
                  Text(t("car.viewPicker.subtitle"),
                      style: TextStyle(fontSize: 12, color: DriverVault.muted(context))),
                  const SizedBox(height: 16),
                  _heading(context, t("car.viewPicker.tabsHeading")),
                  ReorderableListView(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    buildDefaultDragHandles: false,
                    onReorderItem: _moveTab,
                    children: [
                      for (var i = 0; i < _tabRows.length; i++)
                        _row(
                          context,
                          index: i,
                          key: _tabRows[i],
                          label: _tabLabel(_tabRows[i]),
                          on: _visibleTabs.contains(_tabRows[i]),
                          onChanged: (v) => setState(() => v
                              ? _visibleTabs.add(_tabRows[i])
                              : _visibleTabs.remove(_tabRows[i])),
                        ),
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 6),
                    child: Text(
                      "${t("car.viewPicker.alwaysOn", params: {"tab": t("car.tabs.info")})}"
                      " ${t("car.viewPicker.reorderHint")}",
                      style: TextStyle(fontSize: 11, color: DriverVault.muted(context)),
                    ),
                  ),
                  const SizedBox(height: 20),
                  _heading(context, t("car.viewPicker.fieldsHeading")),
                  ReorderableListView(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    buildDefaultDragHandles: false,
                    onReorderItem: _moveField,
                    children: [
                      for (var i = 0; i < _fieldOrder.length; i++)
                        _row(
                          context,
                          index: i,
                          key: _fieldOrder[i],
                          label: t("car.info.${_fieldOrder[i]}"),
                          on: _visibleFields.contains(_fieldOrder[i]),
                          onChanged: (v) => setState(() => v
                              ? _visibleFields.add(_fieldOrder[i])
                              : _visibleFields.remove(_fieldOrder[i])),
                        ),
                    ],
                  ),
                  const SizedBox(height: 20),
                  _heading(context, t("car.viewPicker.serviceColumnsHeading")),
                  ReorderableListView(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    buildDefaultDragHandles: false,
                    onReorderItem: _moveColumn,
                    children: [
                      for (var i = 0; i < _columnOrder.length; i++)
                        _row(
                          context,
                          index: i,
                          key: _columnOrder[i],
                          label: serviceColumnLabel(_columnOrder[i]),
                          // Date is checked and locked rather than left out of
                          // the list: it cannot be switched off, but it can be
                          // moved, and a row missing from here would be a row
                          // nothing on this screen can drag.
                          on: _columnOrder[i] == "date" ||
                              _visibleColumns.contains(_columnOrder[i]),
                          onChanged: _columnOrder[i] == "date"
                              ? null
                              : (v) => setState(() => v
                                  ? _visibleColumns.add(_columnOrder[i])
                                  : _visibleColumns.remove(_columnOrder[i])),
                        ),
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 6),
                    child: Text(
                      "${t("car.viewPicker.columnAlwaysOn", params: {"column": serviceColumnLabel("date")})}"
                      " ${t("car.viewPicker.reorderHint")}",
                      style: TextStyle(fontSize: 11, color: DriverVault.muted(context)),
                    ),
                  ),
                  const SizedBox(height: 20),
                  _heading(context, t("car.viewPicker.servicePartsHeading")),
                  // No drag handle here, and no stored order: the parts are a
                  // checkbox list inside one column, so their position carries
                  // nothing a tab's or a column's does.
                  for (final part in kServiceParts)
                    Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Row(
                        children: [
                          Checkbox(
                            value: _visibleServiceParts.contains(part.key),
                            onChanged: (v) => setState(() => (v ?? false)
                                ? _visibleServiceParts.add(part.key)
                                : _visibleServiceParts.remove(part.key)),
                          ),
                          Expanded(
                            child: Text(t(part.label), style: const TextStyle(fontSize: 14)),
                          ),
                        ],
                      ),
                    ),
                  Padding(
                    padding: const EdgeInsets.only(top: 6),
                    child: Text(
                      t("car.viewPicker.servicePartsHint"),
                      style: TextStyle(fontSize: 11, color: DriverVault.muted(context)),
                    ),
                  ),
                  if (_error != null)
                    Padding(
                      padding: const EdgeInsets.only(top: 12),
                      child: Text(_error!, style: const TextStyle(color: DriverVault.danger)),
                    ),
                ],
              ),
            ),
            SafeArea(
              top: false,
              child: Padding(
                padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: [
                    TextButton(
                      onPressed: _saving ? null : () => Navigator.pop(context),
                      child: Text(t("common.cancel")),
                    ),
                    const SizedBox(width: 8),
                    FilledButton(
                      onPressed: _saving ? null : _save,
                      child: Text(_saving ? t("common.saving") : t("common.save")),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _heading(BuildContext context, String text) => Padding(
        padding: const EdgeInsets.only(bottom: 4),
        child: Text(text.toUpperCase(),
            style: TextStyle(
                fontSize: 11,
                fontWeight: FontWeight.w700,
                letterSpacing: 0.6,
                color: DriverVault.muted(context))),
      );

  /// One switchable, draggable row. The handle is explicit rather than
  /// long-press-anywhere: the row's own tap target is the checkbox, and a
  /// long-press drag would fight it.
  /// A null [onChanged] locks the checkbox on — the row is still draggable,
  /// which is the whole point of showing a column that cannot be switched off.
  Widget _row(
    BuildContext context, {
    required int index,
    required String key,
    required String label,
    required bool on,
    required ValueChanged<bool>? onChanged,
  }) =>
      Padding(
        key: ValueKey(key),
        padding: const EdgeInsets.symmetric(vertical: 2),
        child: Row(
          children: [
            Checkbox(
              value: on,
              onChanged: onChanged == null ? null : (v) => onChanged(v ?? false),
            ),
            Expanded(child: Text(label, style: const TextStyle(fontSize: 14))),
            ReorderableDragStartListener(
              index: index,
              child: Padding(
                padding: const EdgeInsets.all(8),
                child: Icon(Icons.drag_handle, color: DriverVault.muted(context)),
              ),
            ),
          ],
        ),
      );
}
