import { Component, OnDestroy, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Subject, debounceTime, takeUntil } from 'rxjs';
import { AttendanceSignalrService } from '../../services/attendance-signalr.service';
import { AttendanceCategory } from '../../models/attendance.model';

interface CounterRow {
  key: AttendanceCategory;
  labelEn: string;
  labelZh: string;
}

@Component({
  selector: 'app-attendance-counter-page',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './attendance-counter-page.component.html',
  styleUrls: ['./attendance-counter-page.component.scss'],
})
export class AttendanceCounterPageComponent implements OnInit, OnDestroy {
  /**
   * Auto-selected Sunday of the current week, shown in the header.
   * Sunday is the first day of the week, so this is the Sunday on or before today:
   * getDay() returns 0 on Sunday (no shift) and 1–6 on Mon–Sat (step back that many days).
   */
  readonly worshipSunday = this.currentWeekSunday();

  /**
   * Counting is only allowed on the worship day itself. On any other day the page
   * is read-only so volunteers can review the latest count without changing it.
   * getDay() returns 0 on Sunday.
   */
  readonly isSunday = new Date().getDay() === 0;

  /** The Sunday that opens the week containing today. */
  private currentWeekSunday(): Date {
    const now = new Date();
    const sunday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    sunday.setDate(sunday.getDate() - now.getDay());
    return sunday;
  }

  readonly rows: CounterRow[] = [
    { key: 'adult', labelEn: 'Adult', labelZh: '成人'   },
    { key: 'youth', labelEn: 'Youth', labelZh: '青少年' },
    { key: 'kid',   labelEn: 'Kid',   labelZh: '兒童'   },
  ];

  /** The number shown on screen — updated optimistically on every tap. */
  local: Record<AttendanceCategory, number> = { adult: 0, youth: 0, kid: 0 };

  connected = false;

  // The row whose number is being directly edited (null when the dialog is closed).
  editing: CounterRow | null = null;
  // Bound to the dialog's input box while editing.
  editValue: number | null = null;

  // Last authoritative value from the server (per broadcast).
  private server: Record<AttendanceCategory, number> = { adult: 0, youth: 0, kid: 0 };
  // Taps accumulated but not yet sent to the server.
  private pending: Record<AttendanceCategory, number> = { adult: 0, youth: 0, kid: 0 };
  // Deltas sent to the server but not yet acknowledged.
  private inFlight: Record<AttendanceCategory, number> = { adult: 0, youth: 0, kid: 0 };
  // True while an absolute set is awaiting its server ack, so reconcile() doesn't
  // momentarily snap the number back to a stale broadcast.
  private setting: Record<AttendanceCategory, boolean> = { adult: false, youth: false, kid: false };

  // One debounce stream per category so taps on different rows don't cancel each other.
  private readonly flush$: Record<AttendanceCategory, Subject<void>> = {
    adult: new Subject<void>(),
    youth: new Subject<void>(),
    kid: new Subject<void>(),
  };
  private readonly destroy$ = new Subject<void>();

  constructor(private signalr: AttendanceSignalrService) {}

  ngOnInit(): void {
    for (const row of this.rows) {
      this.flush$[row.key]
        .pipe(debounceTime(500), takeUntil(this.destroy$))
        .subscribe(() => this.flush(row.key));
    }

    this.signalr.counts$
      .pipe(takeUntil(this.destroy$))
      .subscribe(counts => {
        if (!counts) {
          return;
        }
        for (const row of this.rows) {
          this.server[row.key] = counts[row.key];
          this.reconcile(row.key);
        }
      });

    this.signalr.start()
      .then(() => (this.connected = true))
      .catch(() => (this.connected = false));
  }

  ngOnDestroy(): void {
    // Flush any taps the volunteer made just before leaving so nothing is lost.
    for (const row of this.rows) {
      this.flush(row.key);
    }
    this.destroy$.next();
    this.destroy$.complete();
    this.signalr.stop();
  }

  /** Displayed value for one age group. */
  display(key: AttendanceCategory): number {
    return this.local[key];
  }

  /** Optimistic +/-: update the screen instantly, debounce the write to the DB. */
  bump(key: AttendanceCategory, step: number): void {
    if (!this.isSunday) {
      return;   // read-only outside the worship day
    }
    if (this.local[key] + step < 0) {
      return;   // never count below zero
    }
    this.local[key] += step;
    this.pending[key] += step;
    this.flush$[key].next();
  }

  /** Open the direct-entry dialog for a row, pre-filled with the current count. */
  openEditor(row: CounterRow): void {
    if (!this.isSunday) {
      return;   // read-only outside the worship day
    }
    this.editing = row;
    this.editValue = this.local[row.key];
  }

  /** Close the dialog without changing anything. */
  cancelEditor(): void {
    this.editing = null;
    this.editValue = null;
  }

  /** Apply the typed number, overwriting the current count for the edited row. */
  confirmEditor(): void {
    if (!this.editing) {
      return;
    }
    const row = this.editing;
    const value = Math.floor(Number(this.editValue));
    // Ignore blank/invalid/negative input — just keep the current value.
    if (!Number.isFinite(value) || value < 0) {
      this.cancelEditor();
      return;
    }
    this.cancelEditor();
    this.setCount(row.key, value);
  }

  // Overwrite one age group with an absolute value: show it instantly, discard any
  // queued taps it supersedes, and push the new value to the server for everyone.
  private setCount(key: AttendanceCategory, value: number): void {
    if (value === this.local[key]) {
      return;   // nothing changed
    }
    this.pending[key] = 0;       // taps before the set no longer matter
    this.local[key] = value;
    this.setting[key] = true;

    this.signalr.setCount(key, value)
      .then(() => {
        this.setting[key] = false;
        this.reconcile(key);
      })
      .catch(() => {
        // Send failed — drop the guard and let the next broadcast correct us.
        this.setting[key] = false;
        this.reconcile(key);
      });
  }

  // Send the accumulated delta for one category as a single batched write.
  private flush(key: AttendanceCategory): void {
    const delta = this.pending[key];
    if (delta === 0) {
      return;
    }
    this.pending[key] = 0;
    this.inFlight[key] += delta;

    this.signalr.increment(key, delta)
      .then(() => {
        this.inFlight[key] -= delta;
        this.reconcile(key);
      })
      .catch(() => {
        // Send failed — re-queue the delta so the count isn't silently dropped.
        this.inFlight[key] -= delta;
        this.pending[key] += delta;
      });
  }

  // Adopt the server's value only when nothing of ours is outstanding, so the
  // local number never momentarily jumps back to a stale total mid-flight.
  private reconcile(key: AttendanceCategory): void {
    if (this.pending[key] === 0 && this.inFlight[key] === 0 && !this.setting[key]) {
      this.local[key] = this.server[key];
    }
  }
}
