import '../../about/external/data/Commitment.dart';
import '../../about/external/data/ExcuseCluster.dart';
import '../../about/external/data/pages/request/CommitmentsRequest.dart';
import '../../about/external/data/pages/request/HistoryRequest.dart';
import '../../about/external/data/pages/request/PageAndSort.dart';
import '../../about/external/data/pages/request/Pageable.dart';
import '../../about/external/data/pages/request/Sort.dart';
import '../../about/external/data/pages/response/CommitmentPage.dart';
import '../../about/external/initial/CompletionRequest.dart';
import '../../about/external/initial/ReportCardRequest.dart';
import '../../about/internal/application/CommitmentClass.dart';
import '../../about/internal/application/Standing.dart';
import '../../about/internal/application/UserDetails.dart';
import '../../utils/DebtEngine.dart';
import '../../utils/GuardrailEngine.dart';
import '../../utils/StandingEngine.dart';
import '../../utils/ToneEngine.dart';
import '../parent/ParentViewModel.dart';
import 'ConnectHome.dart';

class ViewHome extends ParentViewModel {
  ConnectHome connection;

  ViewHome(super.context, this.connection);

  /// Loads the cached user first so the screen never opens on a spinner, then
  /// refreshes everything from the server.
  void initialise() async {
    final UserDetails cached = await getDataManager().getUserDetails();
    connection.onUserLoaded(cached);

    loadPlan();
  }

  void loadPlan() async {
    if (!await hasNetwork(() => loadPlan())) return;

    showLoading("Loading your day");

    try {
      final response = await getDataManager().getTodayPlan(CommitmentsRequest(
        query: PageAndSort(
          sort: Sort('asc', 'dueStart'),
          page: Pageable(0, 0, 50, 0),
        ),
      ));

      final CommitmentPage page = CommitmentPage.fromJson(response.data);

      closeLoading();

      connection.onPlanLoaded(page.content);

      loadOverdue();
    } catch (e) {
      handleError(e, () => loadPlan(), () => dismissError(), "Retry");
    }
  }

  void loadOverdue() async {
    try {
      final response = await getDataManager().getOverdueQueue(HistoryRequest(
        query: PageAndSort(
          sort: Sort('desc', 'dueEnd'),
          page: Pageable(0, 0, 50, 0),
        ),
      ));

      final CommitmentPage page = CommitmentPage.fromJson(response.data);

      connection.onOverdueLoaded(page.content);

      resolveStanding(page.content);

      loadExcuseInsight();
    } catch (e) {
      handleError(e, () => loadOverdue(), () => dismissError(), "Retry");
    }
  }

  /// Standing is derived on device from the same formula the server uses, so
  /// the number on screen is never stale relative to the queue beneath it.
  void resolveStanding(List<Commitment> overdue) async {
    final UserDetails details = await getDataManager().getUserDetails();

    final bool distressed = await _checkDistress();

    final double debt = DebtEngine.totalDebt(overdue);

    final Standing standing = StandingEngine.evaluate(
      debt,
      missedNonNegotiables: DebtEngine.missedNonNegotiables(overdue),
      sickMode: details.sickMode,
      distressed: distressed,
    );

    await getDataManager().setCachedDebtScore(debt);

    connection.onStandingResolved(standing, debt);

    if (distressed) {
      connection.onDistressDetected();
    }
  }

  Future<bool> _checkDistress() async {
    final double previous = await getDataManager().getCachedDebtScore();
    final int opens = await getDataManager().getEngagementCount();
    final double current = await getDataManager().getCachedDebtScore();

    return GuardrailEngine.detectDistress(
      debtDelta: current - previous,
      appOpensThisWeek: opens,
      meanReadiness: 0,
    );
  }

  void loadExcuseInsight() async {
    try {
      final DateTime now = DateTime.now();
      final DateTime start = now.subtract(const Duration(days: 30));

      final response =
          await getDataManager().getExcuseClusters(ReportCardRequest(
        periodStart: start.toIso8601String(),
        periodEnd: now.toIso8601String(),
      ));

      final List<ExcuseCluster> clusters = (response.data as List)
          .map((item) => ExcuseCluster.fromJson(item))
          .toList();

      // Only the strongest pattern is surfaced on the home screen — a wall of
      // findings reads as noise and gets ignored.
      final List<ExcuseCluster> worth =
          clusters.where((cluster) => cluster.insight.isNotEmpty).toList();

      connection.onExcuseInsight(worth.isEmpty ? null : worth.first);
    } catch (e) {
      // The insight is a bonus, never a blocker — a failure here stays silent.
      connection.onExcuseInsight(null);
    }
  }

  /// Ticking a row completes it. The window decides whether that lands as a
  /// clean complete or a late one — the client never claims which.
  void tick(Commitment item) async {
    if (!await hasNetwork(() => tick(item))) return;

    final bool late = item.wouldBeLate;

    try {
      await getDataManager().completeCommitmentEntry(CompletionRequest(
        commitmentId: item.id ?? "",
        proofType: item.proofType.name,
      ));

      connection.onTicked(item, late);

      // Clearing an item moves the debt, so the standing has to be re-derived
      // rather than left showing the number from before the tick.
      loadPlan();
    } catch (e) {
      connection.onTickFailed(item);
      handleError(e, () => tick(item), () => dismissError(), "Retry");
    }
  }

  /// The gate on creating anything new. Grounded blocks everything; Warned
  /// blocks electives only.
  void requestNewCommitment(Standing standing, CommitmentClass intended) async {
    final bool elective = intended == CommitmentClass.Elective;

    if (StandingEngine.permitsNewCommitment(standing, elective: elective)) {
      return;
    }

    if (standing == Standing.Warned && elective) {
      connection.onCreationBlocked(
          "Electives are blocked while you are warned. Clear some debt first.");
      return;
    }

    final UserDetails details = await getDataManager().getUserDetails();

    connection
        .onCreationBlocked(ToneEngine.standingBody(standing, details.tone));
  }
}
