import { Component, OnDestroy, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Subject, takeUntil, firstValueFrom } from 'rxjs';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { DialogsModule } from '@progress/kendo-angular-dialog';
import { buildProofPdf } from '../../services/proof-pdf.builder';
import { OfferingEntryApiService } from '../../services/offering-entry-api.service';
import { OfferingEntrySignalrService } from '../../services/offering-entry-signalr.service';
import {
  GivingCategoryDto, OfferingGivingLineRequest, MemberTypeaheadDto, QuickAddMemberRequest,
  OfferingGivingLineDto, PaymentMethod,
} from '../../models/giving.model';
import { PAYMENT_METHOD_OPTIONS } from '../../../../shared/i18n/option-lists';

interface MemberOption { id: number; displayName: string; }

/** One row of the totals dialog's payment-method breakdown. */
interface MethodSubtotal { method: PaymentMethod; label: string; total: number; }

/**
 * Portrait, phone-friendly page where a volunteer records one Sunday offering
 * at a time. Fields mirror the desktop "Add Giving" form. Each submit persists
 * a single line to the current week's Sunday session (find-or-create, server-
 * side) and the form resets blank for the next entry. No login required.
 */
@Component({
  selector: 'app-offering-entry-mobile-page',
  standalone: true,
  imports: [CommonModule, FormsModule, InputsModule, ButtonsModule, DropDownsModule, DialogsModule],
  templateUrl: './offering-entry-mobile-page.component.html',
  styleUrls: ['./offering-entry-mobile-page.component.scss'],
})
export class OfferingEntryMobilePageComponent implements OnInit, OnDestroy {
  // The Sunday whose offering this session records. The week runs Sunday→Saturday,
  // so a Sunday's gifts can still be keyed any day through the following Saturday:
  // entered on Sat 6/20 → 6/14; on Sun 6/21 (and through Sat 6/27) → 6/21.
  readonly sessionDate = this.sundayOf(new Date());
  private readonly session = this.toIso(this.sessionDate);

  categories: GivingCategoryDto[] = [];
  readonly paymentMethods = PAYMENT_METHOD_OPTIONS;

  memberResults: MemberOption[] = [];
  selectedMemberId: number | null = null;
  selectedMemberName: string | null = null;

  entry: OfferingGivingLineRequest = this.blankEntry();

  // Live running tally for today — seeded by bootstrap, kept current by SignalR
  // (so multiple phones agree) and by each successful submit.
  lineCount = 0;
  systemTotal = 0;

  submitting = false;
  toast: string | null = null;
  connected = false;

  // Quick-add dialog for a giver who isn't on file yet.
  showQuickAdd = false;
  quickAddSaving = false;
  quickAdd: QuickAddMemberRequest = this.blankQuickAdd();

  // Paper-proof dialog: photos/PDFs of the count sheet / envelopes for today's
  // session. Staged here, compressed + merged into one PDF on attach.
  showPaperProof = false;
  paperProofSaving = false;
  paperProofFiles: File[] = [];
  hasProof = false;   // whether today's session already has a proof PDF

  // Totals dialog: opened from the "今日總額" tally. Lines are refetched on open so
  // the breakdown is a fresh cross-phone snapshot, not the (possibly stale) lines
  // loaded at bootstrap.
  showTotals = false;
  totalsLoading = false;
  private totalsLines: OfferingGivingLineDto[] = [];

  private toastTimer?: ReturnType<typeof setTimeout>;
  private readonly destroy$ = new Subject<void>();

  constructor(
    private api: OfferingEntryApiService,
    private signalr: OfferingEntrySignalrService,
  ) {}

  ngOnInit(): void {
    this.api.bootstrap(this.session).subscribe(dto => {
      this.categories = dto.categories;
      this.entry.givingCategoryId = dto.categories[0]?.id ?? 0;
      this.lineCount = dto.summary.lineCount;
      this.systemTotal = dto.summary.systemTotal;
      this.hasProof = dto.summary.hasProof;
    });

    this.signalr.lineAdded$
      .pipe(takeUntil(this.destroy$))
      .subscribe(evt => {
        if (evt.sessionDate !== this.session) {
          return;
        }
        this.lineCount = evt.lineCount;
        this.systemTotal = evt.systemTotal;
      });

    this.signalr.start()
      .then(() => {
        this.connected = true;
        return this.signalr.joinDate(this.session);
      })
      .catch(() => (this.connected = false));
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    if (this.toastTimer) {
      clearTimeout(this.toastTimer);
    }
    this.signalr.leaveDate(this.session)
      .catch(() => undefined)
      .then(() => this.signalr.stop());
  }

  get canSubmit(): boolean {
    if (this.submitting || this.entry.amount <= 0) {
      return false;
    }
    // A named gift must have a giver — only an explicitly anonymous gift may omit one.
    if (!this.entry.isAnonymous && this.entry.memberId == null) {
      return false;
    }
    if (this.entry.paymentMethod === 'Check' && !this.entry.checkNumber) {
      return false;
    }
    return true;
  }

  onMemberFilter(term: string): void {
    if (!term) {
      this.memberResults = [];
      return;
    }
    this.api.searchMembers(term, 10).subscribe(list =>
      this.memberResults = list.map(m => ({ id: m.id, displayName: this.giverLabel(m) })));
  }

  // "NickName LastName (Legal FirstName LastName)" — the legal name in parens so
  // a nick name is never ambiguous. Falls back to the legal name alone when there
  // is no nick name (or it's the same as the legal first name).
  private giverLabel(m: MemberTypeaheadDto): string {
    const legal = `${m.firstName_en} ${m.lastName_en}`.trim();
    const base = (m.nickName && m.nickName !== m.firstName_en)
      ? `${m.nickName} ${m.lastName_en} (${legal})`
      : legal;
    // Append the company / business name so a company-check giver is unambiguous.
    return m.entity ? `${base} · ${m.entity}` : base;
  }

  onMemberSelected(id: number | null): void {
    this.selectedMemberId = id ?? null;
    this.entry.memberId = this.selectedMemberId;
    this.selectedMemberName = this.memberResults.find(m => m.id === id)?.displayName ?? null;
    if (id != null) {
      this.entry.isAnonymous = false;
    }
  }

  markAnonymous(): void {
    this.entry.isAnonymous = true;
    this.entry.memberId = null;
    this.selectedMemberId = null;
    this.selectedMemberName = null;
    this.memberResults = [];
  }

  clearAnonymous(): void {
    this.entry.isAnonymous = false;
  }

  // ── Quick-add member ────────────────────────────────────────────────────────

  openQuickAdd(): void {
    this.quickAdd = this.blankQuickAdd();
    this.showQuickAdd = true;
  }

  cancelQuickAdd(): void {
    this.showQuickAdd = false;
  }

  get canSaveQuickAdd(): boolean {
    return !this.quickAddSaving
      && !!this.quickAdd.firstName_en?.trim()
      && !!this.quickAdd.lastName_en?.trim();
  }

  saveQuickAdd(): void {
    if (!this.canSaveQuickAdd) {
      return;
    }
    this.quickAddSaving = true;
    const request: QuickAddMemberRequest = {
      firstName_en: this.quickAdd.firstName_en.trim(),
      lastName_en: this.quickAdd.lastName_en.trim(),
      nickName: this.trimToNull(this.quickAdd.nickName),
      firstName_zh: this.trimToNull(this.quickAdd.firstName_zh),
      lastName_zh: this.trimToNull(this.quickAdd.lastName_zh),
      entity: this.trimToNull(this.quickAdd.entity),
      phoneCell: this.trimToNull(this.quickAdd.phoneCell),
    };
    this.api.quickAddMember(request).subscribe({
      next: created => {
        this.quickAddSaving = false;
        this.showQuickAdd = false;
        // Seed the new member into the typeahead and select them immediately.
        const option = { id: created.id, displayName: this.giverLabel(created) };
        this.memberResults = [option, ...this.memberResults.filter(m => m.id !== created.id)];
        this.entry.isAnonymous = false;
        this.onMemberSelected(created.id);
        this.showToast('已新增會友 ✓ Member added');
      },
      error: (err: { error?: { message?: string } }) => {
        this.quickAddSaving = false;
        this.showToast(err?.error?.message ?? '新增失敗 Add failed');
      },
    });
  }

  private blankQuickAdd(): QuickAddMemberRequest {
    return {
      firstName_en: '', lastName_en: '', nickName: null,
      firstName_zh: null, lastName_zh: null, entity: null, phoneCell: null,
    };
  }

  private trimToNull(value: string | null): string | null {
    const trimmed = value?.trim();
    return trimmed ? trimmed : null;
  }

  // ── Paper proof ─────────────────────────────────────────────────────────────

  openPaperProof(): void {
    this.paperProofFiles = [];
    this.showPaperProof = true;
  }

  cancelPaperProof(): void {
    this.showPaperProof = false;
  }

  // Shared by the camera and library file inputs — accumulate picks and clear the
  // input so the same file can be re-selected if it was removed.
  onProofFilesSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    const picked = Array.from(input.files ?? []);
    if (picked.length) {
      this.paperProofFiles = [...this.paperProofFiles, ...picked];
    }
    input.value = '';
  }

  removeProofFile(index: number): void {
    this.paperProofFiles = this.paperProofFiles.filter((_, i) => i !== index);
  }

  get canSavePaperProof(): boolean {
    return !this.paperProofSaving && this.paperProofFiles.length > 0;
  }

  async savePaperProof(): Promise<void> {
    if (!this.canSavePaperProof) {
      return;
    }
    this.paperProofSaving = true;
    try {
      const files = [...this.paperProofFiles];
      if (this.hasProof) {
        // Merge into today's existing proof: prepend its pages so the order stays
        // chronological. A 204 (no proof) comes back as an empty Blob — skip it.
        const existing = await firstValueFrom(this.api.downloadProof(this.session));
        if (existing.size > 0) {
          files.unshift(new File([existing], 'existing-proof.pdf', { type: 'application/pdf' }));
        }
      }
      const { blob, skipped } = await buildProofPdf(files);
      await firstValueFrom(this.api.uploadProof(this.session, blob));
      this.hasProof = true;
      this.paperProofSaving = false;
      this.showPaperProof = false;
      this.paperProofFiles = [];
      this.showToast(skipped.length
        ? `已附證明，略過 ${skipped.length} 個不支援檔案 · Attached (${skipped.length} skipped)`
        : '已附紙本證明 ✓ Proof attached');
    } catch (err: unknown) {
      this.paperProofSaving = false;
      const message = (err as { error?: { message?: string } })?.error?.message;
      this.showToast(message ?? '附加失敗 Attach failed');
    }
  }

  // ── Totals dialog ───────────────────────────────────────────────────────────

  openTotals(): void {
    this.showTotals = true;
    this.totalsLoading = true;
    this.totalsLines = [];
    // Refetch so the breakdown reflects every phone's entries at this moment.
    this.api.bootstrap(this.session).subscribe({
      next: dto => {
        this.totalsLines = dto.summary.lines;
        this.totalsLoading = false;
      },
      error: () => {
        this.showTotals = false;
        this.totalsLoading = false;
        this.showToast('讀取總計失敗 Failed to load totals');
      },
    });
  }

  closeTotals(): void {
    this.showTotals = false;
  }

  // One row per payment method that has at least one line, in the canonical
  // PAYMENT_METHOD_OPTIONS order (Cash → Check → Zelle → PayPal → Other).
  get methodSubtotals(): MethodSubtotal[] {
    return PAYMENT_METHOD_OPTIONS
      .map(option => {
        const method = option.value as PaymentMethod;
        const total = this.totalsLines
          .filter(line => line.paymentMethod === method)
          .reduce((sum, line) => sum + line.amount, 0);
        return { method, label: option.label, total };
      })
      .filter(row => row.total > 0);
  }

  get checkLines(): OfferingGivingLineDto[] {
    return this.totalsLines.filter(line => line.paymentMethod === 'Check');
  }

  get checkTotal(): number {
    return this.checkLines.reduce((sum, line) => sum + line.amount, 0);
  }

  get grandTotal(): number {
    return this.totalsLines.reduce((sum, line) => sum + line.amount, 0);
  }

  submit(): void {
    if (!this.canSubmit) {
      return;
    }
    this.submitting = true;
    this.api.appendLine(this.session, this.normalizedLine()).subscribe({
      next: res => {
        this.submitting = false;
        // Server is the source of truth; update now in case our own broadcast
        // hasn't echoed back to this client yet.
        this.lineCount = res.lineCount;
        this.systemTotal = res.systemTotal;
        this.showToast('已登打 ✓ Recorded');
        this.resetForm();
      },
      error: (err: { error?: { message?: string } }) => {
        this.submitting = false;
        this.showToast(err?.error?.message ?? '登打失敗 Submit failed');
      },
    });
  }

  // Send null for fields that don't apply to the chosen method so a stale check
  // number (etc.) from a since-changed method isn't persisted.
  private normalizedLine(): OfferingGivingLineRequest {
    const e = this.entry;
    return {
      memberId: e.isAnonymous ? null : e.memberId,
      givingCategoryId: e.givingCategoryId,
      amount: e.amount,
      paymentMethod: e.paymentMethod,
      checkNumber: e.paymentMethod === 'Check' ? (e.checkNumber || null) : null,
      zelleReferenceCode: e.paymentMethod === 'Zelle' ? (e.zelleReferenceCode || null) : null,
      payPalTransactionId: e.paymentMethod === 'PayPal' ? (e.payPalTransactionId || null) : null,
      isAnonymous: e.isAnonymous,
      notes: e.notes || null,
    };
  }

  private resetForm(): void {
    const defaultCategory = this.categories[0]?.id ?? 0;
    // Keep the last-used payment method — a counter usually enters several gifts
    // of the same method in a row, so they don't have to re-pick it each time.
    // The per-gift reference fields (check #, Zelle, PayPal) still clear below.
    const keepMethod = this.entry.paymentMethod;
    this.entry = this.blankEntry();
    this.entry.givingCategoryId = defaultCategory;
    this.entry.paymentMethod = keepMethod;
    this.selectedMemberId = null;
    this.selectedMemberName = null;
    this.memberResults = [];
  }

  private blankEntry(): OfferingGivingLineRequest {
    return {
      memberId: null, givingCategoryId: 0, amount: 0, paymentMethod: 'Cash',
      checkNumber: null, zelleReferenceCode: null, payPalTransactionId: null,
      isAnonymous: false, notes: null,
    };
  }

  private showToast(message: string): void {
    this.toast = message;
    if (this.toastTimer) {
      clearTimeout(this.toastTimer);
    }
    this.toastTimer = setTimeout(() => (this.toast = null), 2200);
  }

  // The most recent Sunday on or before the given date (Sun→Sat week). getDay()
  // returns 0 for Sunday, so subtracting it lands on this week's Sunday — and on
  // a Sunday it subtracts 0, keeping that same day.
  private sundayOf(d: Date): Date {
    const sunday = new Date(d.getFullYear(), d.getMonth(), d.getDate());
    sunday.setDate(sunday.getDate() - sunday.getDay());
    return sunday;
  }

  // Format using LOCAL date components — NOT toISOString(), which converts to UTC
  // and can roll the date forward a day for behind-UTC users.
  private toIso(d: Date): string {
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    return `${y}-${m}-${day}`;
  }
}
