/**
 * The booking form builder's model, as Form\FormLayout keeps it: one list
 * of fields in the order the guest sees them. The seven core fields stay
 * (hidden at most); the owner adds their own between them.
 */
import { __ } from "./i18n";

export type Answer = "required" | "optional" | "hidden";
export type Width = "full" | "half";
export type CustomType = "text" | "textarea" | "select" | "radio" | "checkbox" | "number" | "date" | "email" | "tel";

export interface Choice {
    value: string;
    label: string;
}

export interface FormEntry {
    id: string;
    /** Empty on a core field: the translated default label shows. */
    label: string;
    placeholder: string;
    help: string;
    width: Width;
    answer: Answer;
    custom: boolean;
    type: CustomType;
    options: Choice[];
    /** The answer shows in the guest's and the owner's emails. */
    email: boolean;
}

/** GET, PUT and DELETE /form-layout. */
export interface FormLayoutPayload {
    fields: FormEntry[];
    default: FormEntry[];
    /** The translated label of each core field. */
    labels: Record<string, string>;
}

export const MAX_FIELDS = 30;
export const MAX_CHOICES = 30;
export const LABEL_MAX = 120;
export const HELP_MAX = 300;

/** A booking cannot be made without these, so they are always asked. */
export const LOCKED = ["date", "party_size", "time", "customer_name"];
/** The guest gives at least one of these, so the restaurant can reach them. */
export const CONTACT = ["phone", "email"];
const CORE = ["date", "party_size", "time", "customer_name", "phone", "email", "notes"];

export const CHOICE_TYPES: CustomType[] = ["select", "radio"];
/** These show no placeholder on the form. */
export const NO_PLACEHOLDER: CustomType[] = ["checkbox", "radio", "date"];

/** A field added since the last save carries a temporary id until it has a label. */
export const TEMP = "__new_";

export const CUSTOM_TYPES: { value: CustomType; label: string }[] = [
    { value: "text", label: __("Short text") },
    { value: "textarea", label: __("Long text") },
    { value: "select", label: __("Dropdown") },
    { value: "radio", label: __("Radio buttons") },
    { value: "checkbox", label: __("Checkbox") },
    { value: "number", label: __("Number") },
    { value: "date", label: __("Date") },
    { value: "email", label: __("Email") },
    { value: "tel", label: __("Phone") },
];

export const typeName = (type: CustomType) => CUSTOM_TYPES.find((t) => t.value === type)?.label ?? __("Short text");

export function blankField(id: string, type: CustomType): FormEntry {
    return {
        id,
        label: "",
        placeholder: "",
        help: "",
        width: "full",
        answer: "optional",
        custom: true,
        type,
        options: CHOICE_TYPES.includes(type) ? [{ value: "", label: "" }, { value: "", label: "" }] : [],
        email: true,
    };
}

/** Ready-made fields for what most restaurants ask, added like any other. */
export const PRESETS: { key: string; name: string; make: (id: string) => FormEntry }[] = [
    {
        key: "email_offers",
        name: __("Email offers"),
        make: (id) => ({
            ...blankField(id, "checkbox"),
            label: __("Send me news and offers by email"),
            help: __("You can unsubscribe at any time."),
        }),
    },
    {
        key: "text_offers",
        name: __("Text offers"),
        make: (id) => ({
            ...blankField(id, "checkbox"),
            label: __("Send me news and offers by text message"),
            help: __("Message and data rates may apply. Reply STOP to opt out."),
        }),
    },
    {
        key: "occasion",
        name: __("Occasion"),
        make: (id) => ({
            ...blankField(id, "select"),
            label: __("Special occasion"),
            options: [
                { value: "birthday", label: __("Birthday") },
                { value: "anniversary", label: __("Anniversary") },
                { value: "date", label: __("Date night") },
                { value: "business", label: __("Business meal") },
            ],
        }),
    },
];

/** The other contact field when this one is phone or email. */
export function partner(id: string): string | null {
    return id === "phone" ? "email" : id === "email" ? "phone" : null;
}

/**
 * Sets one field's answer. Hiding the phone makes the email required, and
 * the other way round, as the server does.
 */
export function setAnswer(fields: FormEntry[], id: string, answer: Answer): FormEntry[] {
    const other = answer === "hidden" ? partner(id) : null;

    return fields.map((entry) =>
        entry.id === id ? { ...entry, answer } : entry.id === other ? { ...entry, answer: "required" } : entry,
    );
}

/** "Gift message" => "gift_message"; accents dropped, so "Dịp đặc biệt" => "dip_dac_biet". */
function slug(label: string, max: number): string {
    return label
        .normalize("NFD")
        .replace(/[̀-ͯ]/g, "")
        .replace(/[đĐ]/g, "d")
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "_")
        .replace(/^_+/, "")
        .slice(0, max)
        .replace(/_+$/, "");
}

function unique(base: string, taken: Set<string>): string {
    let id = base;
    for (let n = 2; taken.has(id); n++) {
        id = `${base}_${n}`;
    }
    taken.add(id);

    return id;
}

export interface LayoutProblem {
    /** The field at fault. */
    id: string;
    /** The setting at fault: `label`, `options`, `answer`... */
    field: string;
    message: string;
}

/**
 * The layout as the server takes it: new fields get their id from their
 * label, choices get a value from theirs, and empty choices are dropped.
 * `renames` maps the temporary ids to the final ones.
 */
export function finalise(fields: FormEntry[]): { fields: FormEntry[]; renames: Map<string, string>; problem: LayoutProblem | null } {
    const taken = new Set<string>(CORE);
    fields.forEach((e) => !e.id.startsWith(TEMP) && taken.add(e.id));
    const renames = new Map<string, string>();
    let problem: LayoutProblem | null = null;

    const out = fields.map((entry) => {
        if (!entry.custom) {
            return { ...entry, label: entry.label.trim(), placeholder: entry.placeholder.trim(), help: entry.help.trim() };
        }
        const label = entry.label.trim();
        if (label === "") {
            problem ??= { id: entry.id, field: "label", message: __("Give the field a label.") };
            return entry;
        }
        let id = entry.id;
        if (id.startsWith(TEMP)) {
            const base = slug(label, 34);
            id = unique(/^[a-z]/.test(base) ? base : `field_${base}`.replace(/_+$/, ""), taken);
            renames.set(entry.id, id);
        }
        let options: Choice[] = [];
        if (CHOICE_TYPES.includes(entry.type)) {
            const values = new Set(entry.options.map((o) => o.value).filter(Boolean));
            options = entry.options
                .filter((o) => o.label.trim() !== "")
                .map((o) => ({ label: o.label.trim(), value: o.value || unique(slug(o.label, 34) || "choice", values) }));
            if (options.length === 0) {
                problem ??= { id: entry.id, field: "options", message: __("Add at least one choice.") };
            }
        }

        return {
            ...entry,
            id,
            label,
            placeholder: NO_PLACEHOLDER.includes(entry.type) ? "" : entry.placeholder.trim(),
            help: entry.help.trim(),
            options,
        };
    });

    return { fields: out, renames, problem };
}

/** `fields.3.options.1` from the server => the field it points at. */
export function problemAt(fields: FormEntry[], path: string, message: string): LayoutProblem | null {
    const [head, index, setting] = path.split(".");
    const entry = head === "fields" && index !== undefined ? fields[Number(index)] : undefined;

    return entry ? { id: entry.id, field: setting ?? "id", message } : null;
}
