"use client";

import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultFilter } from "cmdk";
import { z } from "zod";
import { ExternalLink, Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CommandGroup, CommandItem, CommandShortcut } from "@/components/ui/command";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { FieldGroup } from "@/components/ui/field";
import { Spinner } from "@/components/ui/spinner";
import { FormErrorAlert, SwitchField, TextField } from "@/components/app/form-fields";
import { applyServerErrors } from "@/lib/forms";
import { notify } from "@/app/components/ui/feedback-store";
import { describeError } from "@/app/lib/useAsyncAction";

/**
 * "Add new" for every record picker (SearchableSelect / ComboboxField, AsyncSearchSelect, MultiSelectField).
 *
 * A picker takes one `create` object (build it with useQuickAdd from app/lib/useQuickAdd.js):
 *   { onCreate(name, extra) => Promise<{ value, label, description?, created?, notice?, addedText? }>,   // omit when the user may not add
 *     noun: "buyer",                       // names the row: Add “Zara” as a new buyer
 *     form: { description, fields: [{ name, label, description, default }] },   // extra switches → small dialog
 *     deniedText: "Not in the list? Ask an admin to add the buyer.",          // shown when onCreate is missing
 *     link: { href, label } }              // e.g. "Invite a team member" (opens in a new tab so the form is kept)
 *
 * With typed text and no extra fields, choosing the row (or Enter when nothing matches) creates at once.
 * With nothing typed, or when the record needs more than a name, a small dialog asks for it.
 */

const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);

// Label without a trailing " (CODE) · status" so typing the plain name counts as an exact match.
const plainLabel = (label) => String(label).split(" (")[0].trim().toLowerCase();

/** Options matching the search text, best first (same scoring cmdk uses, on label + description). */
export function matchOptions(options, query) {
  if (!query) return options;
  return options
    .map((option) => ({ option, score: defaultFilter(`${option.label} ${option.description || ""}`, query) }))
    .filter((row) => row.score > 0)
    .sort((a, b) => b.score - a.score)
    .map((row) => row.option);
}

export function hasExactOption(options, query) {
  const text = query.trim().toLowerCase();
  return Boolean(text) && options.some((option) => String(option.label).toLowerCase() === text || plainLabel(option.label) === text);
}

/**
 * Create flow of one picker. `onCreated(option)` selects the new option; `closePicker()` closes the popover
 * when the dialog takes over.
 */
export function usePickerCreate(create, { onCreated, closePicker }) {
  const [creating, setCreating] = useState(false);
  const [dialogName, setDialogName] = useState(null);
  const busy = useRef(false);
  const noun = create?.noun || "option";
  const enabled = Boolean(create?.onCreate);

  const run = async (name, extra) => {
    setCreating(true);
    const option = await create.onCreate(name, extra).finally(() => setCreating(false));
    onCreated(option);
    if (option.created === false) notify.success(`"${option.label}" already exists`, option.notice || "Selected the existing one.");
    else notify.success(`${capitalize(noun)} "${option.label}" added`, option.notice || option.addedText || create.addedText || "It is now available to everyone in your organization.");
    return option;
  };

  // Typed name and nothing else to ask: create now. Otherwise the small dialog asks for the rest.
  const start = async (query) => {
    const name = (query || "").trim();
    if (!enabled || busy.current) return;
    if (!name || create.form?.fields?.length) {
      closePicker?.();
      setDialogName(name);
      return;
    }
    busy.current = true;
    try {
      await run(name);
    } catch (error) {
      notify.error(`Could not add the ${noun}`, describeError(error));
    } finally {
      busy.current = false;
    }
  };

  return { enabled, creating, noun, start, run, dialogName, closeDialog: () => setDialogName(null), config: create };
}

/** The "+ Add …" row. Rendered inside a CommandList; `prominent` when it is the only thing to choose. */
export function PickerCreateItem({ creator, query, prominent = false }) {
  const text = query.trim();
  return <CommandGroup className={prominent ? undefined : "sticky bottom-0 border-t bg-popover"}>
    <CommandItem value={`__create ${text}`} onSelect={() => creator.start(text)} disabled={creator.creating} className="font-medium">
      <span className="flex size-5 shrink-0 items-center justify-center rounded-sm bg-primary text-primary-foreground">{creator.creating ? <Spinner className="size-3 text-primary-foreground" /> : <Plus className="size-3 text-primary-foreground" />}</span>
      <span className="min-w-0 break-words">{text ? <>Add “{text}” as a new {creator.noun}</> : <>Add a new {creator.noun}</>}</span>
      {prominent && text && <CommandShortcut className="tracking-normal">Enter</CommandShortcut>}
    </CommandItem>
  </CommandGroup>;
}

/** A way out that is not an inline create, e.g. "Invite a team member". Opens in a new tab so the form is kept. */
export function PickerLinkItem({ link, prominent = false }) {
  return <CommandGroup className={prominent ? undefined : "sticky bottom-0 border-t bg-popover"}>
    <CommandItem value={`__link ${link.label}`} onSelect={() => window.open(link.href, "_blank", "noopener")} className="font-medium">
      <ExternalLink /> <span className="min-w-0 break-words">{link.label}</span>
    </CommandItem>
  </CommandGroup>;
}

/** Empty list: what was not found, then who can add it when this user cannot (never a dead end). */
export function PickerEmpty({ text, create, canCreate }) {
  const denied = !canCreate && !create?.link && create?.deniedText;
  return <div className="grid gap-1 px-3 pt-3 pb-2 text-sm" role="status">
    <span className="text-muted-foreground">{text}</span>
    {denied && <span className="text-xs text-foreground">{denied}</span>}
  </div>;
}

/** Everything a picker shows below its options: the empty note, the create row, and the link row. */
export function PickerFooter({ creator, create, query, count, exactMatch, emptyText, isLoading = false }) {
  const text = query.trim();
  const isEmpty = count === 0 && !isLoading;
  const showCreate = creator.enabled && !exactMatch && !isLoading;
  return <>
    {isEmpty && <PickerEmpty text={emptyText} create={create} canCreate={creator.enabled} />}
    {showCreate && <PickerCreateItem creator={creator} query={text} prominent={isEmpty} />}
    {create?.link && !isLoading && <PickerLinkItem link={create.link} prominent={isEmpty && !showCreate} />}
  </>;
}

/** Enter in the search box creates when nothing matches (the row is then the only choice). */
export function createOnEnter(event, { creator, query, count, isLoading = false }) {
  if (event.key !== "Enter" || !creator.enabled || isLoading || count > 0 || !query.trim()) return;
  event.preventDefault();
  creator.start(query);
}

const NAME = "picker_new_name";

const dialogSchema = z.object({
  [NAME]: z.string().trim().min(2, "Enter a name of at least 2 characters.").max(120, "Keep the name under 120 characters."),
  extra: z.record(z.string(), z.boolean()),
});

/** Small dialog used when nothing was typed or the record needs more than a name. Render it beside the Popover. */
export function PickerCreateDialog({ creator }) {
  const open = creator.dialogName !== null;
  return <Dialog open={open} onOpenChange={(next) => { if (!next && !creator.creating) creator.closeDialog(); }}>
    <DialogContent className="sm:max-w-md">
      {open && <PickerCreateForm creator={creator} />}
    </DialogContent>
  </Dialog>;
}

function PickerCreateForm({ creator }) {
  const fields = creator.config.form?.fields || [];
  const form = useForm({
    resolver: zodResolver(dialogSchema),
    defaultValues: { [NAME]: creator.dialogName || "", extra: Object.fromEntries(fields.map((field) => [field.name, field.default !== false])) },
  });
  const saving = form.formState.isSubmitting;

  const submit = form.handleSubmit(async (values) => {
    try {
      await creator.run(values[NAME].trim(), values.extra);
      creator.closeDialog();
    } catch (error) {
      applyServerErrors(form, error, (key) => (key === "name" || key === "label" || key === "code" ? NAME : key));
    }
  });

  // The dialog is portalled but still inside the page's <form> in the React tree: keep this submit to itself.
  return <form onSubmit={(event) => { event.stopPropagation(); submit(event); }} noValidate className="grid gap-5">
    <DialogHeader>
      <DialogTitle>Add a new {creator.noun}</DialogTitle>
      <DialogDescription>{creator.config.form?.description || "Only the name is needed. It is selected here as soon as it is added."}</DialogDescription>
    </DialogHeader>
    <FormErrorAlert form={form} />
    <FieldGroup className="gap-4">
      <TextField control={form.control} name={NAME} label={`${capitalize(creator.noun)} name`} inputProps={{ autoFocus: true, autoComplete: "off" }} />
      {fields.map((field) => <SwitchField key={field.name} control={form.control} name={`extra.${field.name}`} label={field.label} description={field.description} />)}
    </FieldGroup>
    <DialogFooter>
      <Button type="button" variant="outline" disabled={saving} onClick={creator.closeDialog}>Cancel</Button>
      <Button type="submit" disabled={saving}>{saving ? <Spinner /> : <Plus />} Add {creator.noun}</Button>
    </DialogFooter>
  </form>;
}
