'use client';

import { useEffect, useRef, useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { AlertTriangle, Loader2, Upload, X } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { DatePicker } from '@/components/ui/date-picker';
import { Textarea } from '@/components/ui/textarea';
import { FormErrorSummary } from '@/components/forms/form-error-summary';
import { useFormScrollToError } from '@/hooks/use-form-scroll-to-error';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
import { CurrencyInput } from '@/components/shared/currency-input';
import { CurrencySelect } from '@/components/shared/currency-select';
import { TripLabelCombobox } from '@/components/expenses/trip-label-combobox';
import { UserPicker } from '@/components/shared/user-picker';
import { apiFetch } from '@/lib/api/client';
import type { z } from 'zod';
import { createExpenseSchema, type CreateExpenseInput } from '@/lib/validators/expenses';
import { EXPENSE_CATEGORIES, PAYMENT_METHODS, formatEnum } from '@/lib/constants';
import type { ExpenseRow } from './expense-columns';

interface UploadedReceipt {
  id: string;
  filename: string;
}

interface ExpenseFormDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  expense?: ExpenseRow;
}

export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDialogProps) {
  const queryClient = useQueryClient();
  const isEdit = !!expense;

  // Per-port vocabulary override for expense categories. Falls back to
  // the shipped EXPENSE_CATEGORIES constant when /api/v1/vocabularies
  // hasn't loaded yet or returns malformed data - keeps the picker
  // populated during the first render.
  const { data: vocab } = useQuery<{ data: Record<string, readonly string[]> }>({
    queryKey: ['vocabularies'],
    queryFn: () => apiFetch('/api/v1/vocabularies'),
    staleTime: 5 * 60_000,
  });
  const categoryList = vocab?.data?.expense_categories ?? EXPENSE_CATEGORIES;
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [uploadedReceipt, setUploadedReceipt] = useState<UploadedReceipt | null>(null);
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [noReceipt, setNoReceipt] = useState(false);
  const [uploadError, setUploadError] = useState<string | null>(null);
  const [isUploading, setIsUploading] = useState(false);

  const {
    register,
    handleSubmit,
    setValue,
    reset,
    watch,
    control,
    formState: { errors, isSubmitting },
  } = useForm<z.input<typeof createExpenseSchema>, unknown, CreateExpenseInput>({
    resolver: zodResolver(createExpenseSchema),
    defaultValues: {
      currency: 'USD',
      paymentStatus: 'unpaid',
    },
  });
  // Scroll-to-first-error wrapper around handleSubmit. On validation
  // failure it auto-scrolls + focuses the first errored input so reps
  // can see what failed without hunting (especially important on tall
  // drawer-based forms like this one).
  const onSubmitWithScroll = useFormScrollToError(handleSubmit, errors);

  useEffect(() => {
    if (open && expense) {
      reset({
        establishmentName: expense.establishmentName ?? undefined,
        amount: Number(expense.amount),
        currency: expense.currency,
        category: expense.category as CreateExpenseInput['category'],
        paymentMethod: expense.paymentMethod as CreateExpenseInput['paymentMethod'],
        payer: expense.payer ?? undefined,
        expenseDate: new Date(expense.expenseDate),
        paymentStatus: (expense.paymentStatus as CreateExpenseInput['paymentStatus']) ?? 'unpaid',
        tripLabel: expense.tripLabel ?? undefined,
        noReceiptAcknowledged: Boolean(expense.noReceiptAcknowledged),
      });
      setUploadedReceipt(null);
      setPreviewUrl(null);
      setNoReceipt(Boolean(expense.noReceiptAcknowledged));
      setUploadError(null);
    } else if (open && !expense) {
      reset({
        currency: 'USD',
        paymentStatus: 'unpaid',
        expenseDate: new Date(),
        noReceiptAcknowledged: false,
      });
      setUploadedReceipt(null);
      setPreviewUrl(null);
      setNoReceipt(false);
      setUploadError(null);
    }
  }, [open, expense, reset]);

  // Capture the URL inside the effect closure so the cleanup revokes the
  // URL it observed at mount, not the one captured by a later render.
  // Audit caught a bug where the cleanup ran on every change and revoked
  // the URL that was still being shown.
  useEffect(() => {
    const url = previewUrl;
    return () => {
      if (url) URL.revokeObjectURL(url);
    };
  }, [previewUrl]);

  // Reset upload state whenever the sheet closes - re-opening on the same
  // expense was carrying stale state from the prior session.
  useEffect(() => {
    if (!open) {
      setUploadedReceipt(null);
      setPreviewUrl(null);
      setNoReceipt(false);
      setUploadError(null);
      setIsUploading(false);
      if (fileInputRef.current) fileInputRef.current.value = '';
    }
  }, [open]);

  const mutation = useMutation({
    mutationFn: (data: CreateExpenseInput) => {
      if (isEdit) {
        return apiFetch(`/api/v1/expenses/${expense.id}`, {
          method: 'PATCH',
          body: data,
        });
      }
      return apiFetch('/api/v1/expenses', { method: 'POST', body: data });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['expenses'] });
      onOpenChange(false);
    },
  });

  async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;
    setUploadError(null);
    if (previewUrl) URL.revokeObjectURL(previewUrl);
    setPreviewUrl(URL.createObjectURL(file));
    setIsUploading(true);
    try {
      const formData = new FormData();
      formData.append('file', file);
      formData.append('category', 'receipt');
      const res = await fetch('/api/v1/files/upload', {
        method: 'POST',
        body: formData,
        credentials: 'include',
      });
      if (!res.ok) throw new Error('Upload failed');
      const json = (await res.json()) as { data: { id: string; filename: string } };
      setUploadedReceipt({ id: json.data.id, filename: json.data.filename });
      setNoReceipt(false);
      // Keep form state in sync so the schema-level refine that requires
      // receiptFileIds.length > 0 || noReceiptAcknowledged === true sees
      // a populated value at validation time.
      setValue('receiptFileIds', [json.data.id], { shouldValidate: true });
      setValue('noReceiptAcknowledged', false, { shouldValidate: true });
    } catch (err) {
      setUploadError(err instanceof Error ? err.message : 'Upload failed');
      setUploadedReceipt(null);
      setValue('receiptFileIds', undefined, { shouldValidate: true });
    } finally {
      setIsUploading(false);
    }
  }

  function clearReceipt() {
    if (previewUrl) URL.revokeObjectURL(previewUrl);
    setPreviewUrl(null);
    setUploadedReceipt(null);
    setUploadError(null);
    if (fileInputRef.current) fileInputRef.current.value = '';
    setValue('receiptFileIds', undefined, { shouldValidate: true });
  }

  function onSubmit(data: CreateExpenseInput) {
    mutation.mutate({
      ...data,
      receiptFileIds: uploadedReceipt ? [uploadedReceipt.id] : undefined,
      noReceiptAcknowledged: Boolean(noReceipt && !uploadedReceipt),
    });
  }

  const canSubmit = isEdit || Boolean(uploadedReceipt) || noReceipt;

  return (
    <Sheet open={open} onOpenChange={onOpenChange}>
      <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
        <SheetHeader>
          <SheetTitle>{isEdit ? 'Edit Expense' : 'New Expense'}</SheetTitle>
        </SheetHeader>

        <form onSubmit={onSubmitWithScroll(onSubmit)} className="space-y-4 mt-4">
          <FormErrorSummary
            errors={errors}
            labels={{
              expenseDate: 'Date',
              amount: 'Amount',
              currency: 'Currency',
              category: 'Category',
              paymentMethod: 'Payment method',
              receiptFileIds: 'Receipt',
              noReceiptAcknowledged: 'Receipt',
            }}
          />
          <div className="space-y-2">
            <Label htmlFor="expenseDate">Date *</Label>
            <Controller
              control={control}
              name="expenseDate"
              defaultValue={
                expense?.expenseDate
                  ? (new Date(expense.expenseDate) as unknown as Date)
                  : (new Date() as unknown as Date)
              }
              render={({ field }) => {
                // Schema stores expenseDate as a Date; the picker speaks
                // YYYY-MM-DD strings. Bridge both directions on the fly so
                // upstream validation + downstream API payload stay
                // unchanged.
                const isoValue =
                  field.value instanceof Date
                    ? field.value.toISOString().split('T')[0]
                    : typeof field.value === 'string'
                      ? field.value.split('T')[0]
                      : '';
                return (
                  <DatePicker
                    id="expenseDate"
                    value={isoValue}
                    onChange={(v) => field.onChange(v ? new Date(v) : undefined)}
                  />
                );
              }}
            />
            {errors.expenseDate && (
              <p className="text-xs text-destructive">{errors.expenseDate.message}</p>
            )}
          </div>

          <div className="grid grid-cols-2 gap-3">
            <div className="space-y-2">
              <Label htmlFor="amount">Amount *</Label>
              <CurrencyInput
                id="amount"
                value={(watch('amount') as number | null | undefined) ?? ''}
                currency={watch('currency') ?? 'USD'}
                onChange={(v) =>
                  setValue('amount', v ?? Number.NaN, { shouldDirty: true, shouldValidate: true })
                }
              />
              {errors.amount && <p className="text-xs text-destructive">{errors.amount.message}</p>}
            </div>

            <div className="space-y-2">
              <Label htmlFor="currency">Currency</Label>
              <CurrencySelect
                id="currency"
                value={watch('currency') ?? 'USD'}
                onValueChange={(v) => setValue('currency', v, { shouldDirty: true })}
              />
              {errors.currency && (
                <p className="text-xs text-destructive">{errors.currency.message}</p>
              )}
            </div>
          </div>

          <div className="space-y-2">
            <Label htmlFor="establishmentName">Establishment</Label>
            <Input
              id="establishmentName"
              placeholder="Establishment name"
              {...register('establishmentName')}
            />
          </div>

          <div className="space-y-2">
            <Label htmlFor="category">Category</Label>
            <Select
              onValueChange={(v) => setValue('category', v as CreateExpenseInput['category'])}
              defaultValue={expense?.category ?? undefined}
            >
              <SelectTrigger id="category">
                <SelectValue placeholder="Select category" />
              </SelectTrigger>
              <SelectContent>
                {categoryList.map((cat) => (
                  <SelectItem key={cat} value={cat}>
                    {formatEnum(cat)}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>

          <div className="space-y-2">
            <Label htmlFor="paymentMethod">Payment Method</Label>
            <Select
              onValueChange={(v) =>
                setValue('paymentMethod', v as CreateExpenseInput['paymentMethod'])
              }
              defaultValue={expense?.paymentMethod ?? undefined}
            >
              <SelectTrigger id="paymentMethod">
                <SelectValue placeholder="Select method" />
              </SelectTrigger>
              <SelectContent>
                {PAYMENT_METHODS.map((m) => (
                  <SelectItem key={m} value={m}>
                    {formatEnum(m)}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>

          <div className="space-y-2">
            <Label htmlFor="payer">Payer</Label>
            <UserPicker
              value={(watch('payer') as string | undefined) ?? null}
              onChange={(v) => setValue('payer', v ?? '', { shouldDirty: true })}
              placeholder="Who paid?"
            />
            <p className="text-xs text-muted-foreground">
              Pick a teammate or choose &ldquo;Other&hellip;&rdquo; to type any name.
            </p>
          </div>

          <div className="space-y-2">
            <Label htmlFor="paymentStatus">Payment Status</Label>
            <Select
              onValueChange={(v) => setValue('paymentStatus', v as 'unpaid' | 'paid' | 'partial')}
              defaultValue={expense?.paymentStatus ?? 'unpaid'}
            >
              <SelectTrigger id="paymentStatus">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="unpaid">Unpaid</SelectItem>
                <SelectItem value="paid">Paid</SelectItem>
                <SelectItem value="partial">Partial</SelectItem>
              </SelectContent>
            </Select>
          </div>

          <div className="space-y-2">
            <Label htmlFor="tripLabel">Trip / event</Label>
            <TripLabelCombobox
              id="tripLabel"
              value={watch('tripLabel') ?? ''}
              onChange={(label) => setValue('tripLabel', label ?? undefined, { shouldDirty: true })}
              placeholder="e.g. Palm Beach 2026 (optional)"
            />
            <p className="text-xs text-muted-foreground">
              Group expenses by yacht show or business trip. Pick a past trip or type a new one.
            </p>
          </div>

          <div className="space-y-2">
            <Label htmlFor="description">Description</Label>
            <Textarea
              id="description"
              placeholder="Additional notes..."
              rows={3}
              {...register('description')}
            />
          </div>

          {!isEdit && (
            <div className="space-y-2 rounded-md border p-3">
              <Label className="text-sm font-medium">Receipt</Label>
              {previewUrl ? (
                <div className="relative">
                  <img
                    src={previewUrl}
                    alt="Receipt preview"
                    className="max-h-48 rounded border object-contain"
                  />
                  <button
                    type="button"
                    onClick={clearReceipt}
                    aria-label="Remove receipt"
                    className="absolute top-1 right-1 rounded-full bg-background/90 hover:bg-background border p-1 shadow-sm"
                  >
                    <X className="h-3 w-3" />
                  </button>
                  <p className="mt-1 text-xs text-muted-foreground">
                    {isUploading
                      ? 'Uploading...'
                      : uploadedReceipt
                        ? `Uploaded: ${uploadedReceipt.filename}`
                        : 'Selecting...'}
                  </p>
                </div>
              ) : (
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  className="w-full"
                  disabled={noReceipt}
                  onClick={() => fileInputRef.current?.click()}
                >
                  <Upload className="mr-2 h-4 w-4" aria-hidden />
                  Upload receipt image or PDF
                </Button>
              )}
              <input
                ref={fileInputRef}
                type="file"
                accept="image/*,application/pdf"
                className="hidden"
                onChange={handleFileChange}
              />
              {uploadError && <p className="text-xs text-destructive">{uploadError}</p>}
              {/* `receiptFileIds` is set via `setValue`, not `register`,
                  so it has no DOM input the auto-scroll-to-error helper
                  can focus. Without this surface the schema's refine
                  (receiptFileIds.length > 0 || noReceiptAcknowledged)
                  would fail invisibly and Submit would silently do
                  nothing. Bucket-4 #7 fix. */}
              {errors.receiptFileIds?.message ? (
                <p role="alert" className="text-xs text-destructive">
                  {String(errors.receiptFileIds.message)}
                </p>
              ) : null}

              <div className="flex items-start gap-2 pt-1">
                <Checkbox
                  id="noReceipt"
                  checked={noReceipt}
                  onCheckedChange={(checked) => {
                    const next = checked === true;
                    setNoReceipt(next);
                    if (next) clearReceipt();
                    setValue('noReceiptAcknowledged', next, { shouldValidate: true });
                  }}
                />
                <Label htmlFor="noReceipt" className="text-sm font-normal leading-tight">
                  I have no receipt for this expense
                </Label>
              </div>

              {noReceipt && (
                <div className="flex gap-2 rounded-md border border-amber-300 bg-amber-50 p-2 text-xs text-amber-900 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-200">
                  <AlertTriangle className="h-4 w-4 shrink-0" aria-hidden />
                  <span>
                    Expenses without a receipt may not be reimbursed by the parent company. The PDF
                    export will flag this expense.
                  </span>
                </div>
              )}
            </div>
          )}

          {mutation.isError && (
            <p className="text-sm text-destructive">{(mutation.error as Error).message}</p>
          )}

          <SheetFooter className="pt-2">
            <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
              Cancel
            </Button>
            <Button
              type="submit"
              disabled={isSubmitting || mutation.isPending || isUploading || !canSubmit}
            >
              {(isSubmitting || mutation.isPending) && (
                <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
              )}
              {isEdit ? 'Save changes' : 'Create Expense'}
            </Button>
          </SheetFooter>
        </form>
      </SheetContent>
    </Sheet>
  );
}
