import { effect, inject, Injectable, signal } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { first, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { selectAllTasksWithDueTime } from '../tasks/store/task.selectors';
import { Store } from '@ngrx/store';
import { CalendarIntegrationService } from '../calendar-integration/calendar-integration.service';
import { PlannerDay } from './planner.model';
import { selectPlannerDays } from './store/planner.selectors';
import { TaskWithDueTime } from '../tasks/task.model';
import { DateService } from '../../core/date/date.service';
import { GlobalTrackingIntervalService } from '../../core/global-tracking-interval/global-tracking-interval.service';
import { selectTodayTaskIds } from '../work-context/store/work-context.selectors';
import { msToString } from '../../ui/duration/ms-to-string.pipe';
import { getDbDateStr } from '../../util/get-db-date-str';
import { parseDbDateStr } from '../../util/parse-db-date-str';
import { getDiffInDays } from '../../util/get-diff-in-days';
import { selectActiveTaskRepeatCfgs } from '../task-repeat-cfg/store/task-repeat-cfg.selectors';
import { Log } from '../../core/log';
import { LayoutService } from '../../core-ui/layout/layout.service';

@Injectable({
  providedIn: 'root',
})
export class PlannerService {
  private static readonly INITIAL_DAYS_DESKTOP = 15;
  private static readonly INITIAL_DAYS_MOBILE = 5;
  private static readonly AUTO_LOAD_INCREMENT = 7;

  private _store = inject(Store);
  private _calendarIntegrationService = inject(CalendarIntegrationService);
  private _dateService = inject(DateService);
  private _globalTrackingIntervalService = inject(GlobalTrackingIntervalService);
  private _layoutService = inject(LayoutService);

  private _userHasScrolled = signal(false);

  private _daysToShowCount$ = new BehaviorSubject<number>(
    this._layoutService.isXs()
      ? PlannerService.INITIAL_DAYS_MOBILE
      : PlannerService.INITIAL_DAYS_DESKTOP,
  );
  public isLoadingMore$ = new BehaviorSubject<boolean>(false);
  private _loadMoreTimeoutId: ReturnType<typeof setTimeout> | null = null;

  includedWeekDays$ = of([0, 1, 2, 3, 4, 5, 6]);

  constructor() {
    // Reset to initial count when breakpoint changes, but only if user hasn't scrolled yet
    effect(() => {
      const isMobile = this._layoutService.isXs();

      // Only reset to initial count if user hasn't manually scrolled yet
      if (!this._userHasScrolled()) {
        const newCount = isMobile
          ? PlannerService.INITIAL_DAYS_MOBILE
          : PlannerService.INITIAL_DAYS_DESKTOP;
        this._daysToShowCount$.next(newCount);
      }
    });
  }

  daysToShow$ = combineLatest([
    this._daysToShowCount$,
    this._globalTrackingIntervalService.todayDateStr$,
    this.includedWeekDays$,
  ]).pipe(
    tap(([count, todayStr]) => Log.log('daysToShow$', { count, todayStr })),
    map(([count, _, includedWeekDays]) => {
      // Guard against empty includedWeekDays to prevent infinite loop
      if (includedWeekDays.length === 0) {
        return [];
      }

      const today = new Date().getTime();
      const daysToShow: string[] = [];

      // CRITICAL FIX: Loop until we have the required count of days
      // (not just iterate N times which produces fewer days if weekends are excluded)
      let daysAdded = 0;
      let offset = 0;
      while (daysAdded < count) {
        // eslint-disable-next-line no-mixed-operators
        const dayOfWeek = new Date(today + offset * 24 * 60 * 60 * 1000).getDay();
        if (includedWeekDays.includes(dayOfWeek)) {
          daysToShow.push(
            // eslint-disable-next-line no-mixed-operators
            this._dateService.todayStr(today + offset * 24 * 60 * 60 * 1000),
          );
          daysAdded++;
        }
        offset++;
      }

      return daysToShow;
    }),
  );

  allDueWithTimeTasks$: Observable<TaskWithDueTime[]> = this._store.select(
    selectAllTasksWithDueTime,
  );

  // TODO this needs to be more performant
  days$: Observable<PlannerDay[]> = this.daysToShow$.pipe(
    switchMap((daysToShow) =>
      combineLatest([
        this._store.select(selectActiveTaskRepeatCfgs),
        this._store.select(selectTodayTaskIds),
        this._calendarIntegrationService.calendarEvents$,
        this.allDueWithTimeTasks$,
        this._globalTrackingIntervalService.todayDateStr$,
      ]).pipe(
        switchMap(
          ([
            taskRepeatCfgs,
            todayListTaskIds,
            calendarEvents,
            allTasksPlanned,
            todayStr,
          ]) =>
            this._store.select(
              selectPlannerDays(
                daysToShow,
                taskRepeatCfgs,
                todayListTaskIds,
                calendarEvents,
                allTasksPlanned,
                todayStr,
              ),
            ),
        ),
      ),
    ),
    // for better performance
    // TODO better solution, gets called very often
    // tap((val) => Log.log('days$', val)),
    // tap((val) => Log.log('days$ SIs', val[0]?.scheduledIItems)),
    shareReplay({ bufferSize: 1, refCount: true }),
  );
  tomorrow$ = this.days$.pipe(
    map((days) => {
      const tomorrowStr = getDbDateStr(this._dateService.getLogicalTomorrowMs());
      return days.find((d) => d.dayDate === tomorrowStr) ?? null;
    }),
    shareReplay({ bufferSize: 1, refCount: true }),
  );

  // plannedTaskDayMap$: Observable<{ [taskId: string]: string }> = this._store
  //   .select(selectTaskIdPlannedDayMap)
  //   // make this more performant by sharing stream
  //   .pipe(shareReplay(1));

  getDayOnce$(dayStr: string): Observable<PlannerDay | undefined> {
    return this.days$.pipe(
      map((days) => days.find((d) => d.dayDate === dayStr)),
      first(),
    );
  }

  getSnackExtraStr(dayStr: string): Promise<string> {
    return this.getDayOnce$(dayStr)
      .pipe(
        map((day) => {
          if (!day) {
            return '';
          }
          if (day.timeEstimate === 0) {
            return ` – ∑ ${day.itemsTotal}`;
          }

          return `<br />∑ ${day.itemsTotal} ｜ ${msToString(day.timeEstimate)}`;
        }),
      )
      .toPromise();
  }

  loadMoreDays(): void {
    this.isLoadingMore$.next(true);
    this._userHasScrolled.set(true);

    // Yield to event loop to ensure loading state is visible
    this._loadMoreTimeoutId = setTimeout(() => {
      this._loadMoreTimeoutId = null;
      const currentCount = this._daysToShowCount$.value;
      this._daysToShowCount$.next(currentCount + PlannerService.AUTO_LOAD_INCREMENT);
      this.isLoadingMore$.next(false);
    }, 0);
  }

  ensureDayLoaded(dayDate: string): void {
    const target = parseDbDateStr(dayDate);
    const diff = getDiffInDays(this._dateService.getLogicalTodayDate(), target);
    if (diff >= 0 && diff >= this._daysToShowCount$.value) {
      this._userHasScrolled.set(true);
      this._daysToShowCount$.next(diff + 3);
    }
  }

  resetScrollState(): void {
    if (this._loadMoreTimeoutId !== null) {
      clearTimeout(this._loadMoreTimeoutId);
      this._loadMoreTimeoutId = null;
    }
    this.isLoadingMore$.next(false);
    this._userHasScrolled.set(false);
    this._daysToShowCount$.next(
      this._layoutService.isXs()
        ? PlannerService.INITIAL_DAYS_MOBILE
        : PlannerService.INITIAL_DAYS_DESKTOP,
    );
  }
}
