import React, { useEffect, useRef, useState } from "react";
import axios from "axios";
import { Button } from "@/prime-react";
import { Menu } from "@/prime-react";
import { Column } from "@/prime-react";
import { DataTable } from "@/prime-react";
import { Dialog } from "@/prime-react";
import { Dropdown } from "@/prime-react";
import { InputText } from "@/prime-react";
import { InputTextarea } from "@/prime-react";
import { InputNumber } from "@/prime-react";
import { MultiSelect } from "@/prime-react";
import { Message } from "@/prime-react";
import { ProgressSpinner } from "@/prime-react";
import { Tag } from "@/prime-react";
import DetailForm from "@/shared/DetailForm";
import { FLOW_STATUS, SERVICE_STATUS, SERVICE_STATUS_LABEL, SERVICE_SUPPLIER, SERVICE_VISIBILITY, SERVICE_VISIBILITY_LABEL, USER_TYPE_SIDE, USER_TYPE_SIDE_LABEL } from "@/shared/status";
import { notifyError, notifySuccess } from "../shared/toast";
import { NewFlowModal } from "./Chatflows";
import RefreshButton from "./shared/RefreshButton";

// The name the org gave it; the handle is the fallback for a flow that predates naming.
const flowLabel = (flow) => (flow?.name ?? "").trim() || (flow?.flow_key ?? "").trim() || `Flow #${flow?.id}`;

const Service = ({ onNavigate }) => {
    const [items, setItems] = useState([]);
    const [types, setTypes] = useState([]);
    const [resources, setResources] = useState([]);
    // Published chatflows, for binding one to a service. Draft flows are listed too but labelled:
    // an org normally binds the flow it is about to publish.
    const [flows, setFlows] = useState([]);
    // The service form is HIDDEN while this is open, not stacked: shared/Modal sits at z-index
    // 1000, under PrimeReact's dialog, so a nested one would render behind the form that opened it.
    // `edit` state survives the round trip, so nothing typed is lost.
    const [flowModal, setFlowModal] = useState(false);
    const [loading, setLoading] = useState(true);

    // ONE popup menu for the whole table, pointed at whichever row opened it — a Menu per row
    // would mount as many overlays as there are services.
    const menuRef = useRef(null);
    const [menuRow, setMenuRow] = useState(null);

    // Per-service detail values (`info`), gated by that service's own service_detail_config.
    // Separate dialog from the service form: the config is per service and admin-owned, so the
    // fields differ from one service to the next.
    const [detail, setDetail] = useState({ open: false, service: null, schema: [], info: {}, busy: false });

    const openDetail = async (row) => {
        setDetail({ open: true, service: row, schema: [], info: {}, busy: true });
        try {
            const { data } = await axios.get("/service_detail/get", { params: { service_id: row.id } });
            setDetail(d => ({ ...d, schema: data.schema || [], info: data.info || {}, busy: false }));
        } catch (err) {
            setDetail(d => ({ ...d, busy: false }));
            notifyError(err.response?.data?.message || "Could not load service details.");
        }
    };

    const saveDetail = async () => {
        setDetail(d => ({ ...d, busy: true }));
        try {
            await axios.put("/service_detail/save", { service_id: detail.service.id, info: detail.info });
            setDetail(d => ({ ...d, open: false, busy: false }));
            notifySuccess("Service details saved.");
            // Reload, or the "N details needed" badge keeps naming what was just answered.
            await load();
        } catch (err) {
            setDetail(d => ({ ...d, busy: false }));
            const errors = err.response?.data?.errors?.info;
            notifyError(Array.isArray(errors) ? errors.join(" ") : (err.response?.data?.message || "Could not save."));
        }
    };

    // Roster for one service: who joined and as which role. The table previously showed only a
    // count, so there was no way to see or manage the people behind it.
    const [roster, setRoster] = useState({ open: false, service: null, rows: [], busy: false });

    const openRoster = async (row) => {
        setRoster({ open: true, service: row, rows: [], busy: true });
        try {
            const { data } = await axios.get("/service/participants", { params: { service_id: row.id } });
            setRoster(r => ({ ...r, rows: data.participants || [], busy: false }));
        } catch (err) {
            setRoster(r => ({ ...r, busy: false }));
            notifyError(err.response?.data?.message || "Could not load participants.");
        }
    };

    const removeParticipant = async (userId) => {
        setRoster(r => ({ ...r, busy: true }));
        try {
            await axios.post("/service/participant/remove", { service_id: roster.service.id, user_id: userId });
            const { data } = await axios.get("/service/participants", { params: { service_id: roster.service.id } });
            setRoster(r => ({ ...r, rows: data.participants || [], busy: false }));
            notifySuccess("Removed from the service.");
            await load();
        } catch (err) {
            setRoster(r => ({ ...r, busy: false }));
            notifyError(err.response?.data?.message || "Could not remove.");
        }
    };

    const [edit, setEdit] = useState({
        open: false, id: null,
        name: "", short_name: "", description: "", price: 0,
        user_type_ids: [], resource_ids: [],
        busy: false, error: null, info: null,
    });

    const load = async () => {
        setLoading(true);
        try {
            const [s, t, r, f] = await Promise.all([
                axios.get("/service/list"),
                axios.get("/user_type/list"),
                axios.get("/resource/list"),
                axios.get("/flow/list").catch(() => ({ data: {} })),
            ]);
            setItems(s.data || []);
            setTypes((t.data || []).filter(x => x.enabled));
            setResources(r.data?.resources || []);
            // /flow/list answers with the {data,…} envelope; the bare array is the pre-envelope
            // shape, kept as a fallback rather than assumed.
            setFlows(f.data?.data ?? (Array.isArray(f.data) ? f.data : []));
        } catch (err) { console.error(err); }
        finally { setLoading(false); }
    };

    useEffect(() => { load(); }, []);

    const openCreate = () => setEdit({
        open: true, id: null,
        name: "", short_name: "", description: "", price: 0,
        visibility: SERVICE_VISIBILITY.ORG_ONLY, user_type_ids: [],
        supplier: SERVICE_SUPPLIER.ORG, supplier_user_type_ids: [],
        resource_ids: [], flow_id: null, flow_template: null,
        busy: false, error: null, info: null,
    });

    const openEdit = (row) => setEdit({
        open: true, id: row.id,
        name: row.name, short_name: row.short_name || "", description: row.description || "", price: parseFloat(row.price) || 0,
        visibility: row.visibility ?? SERVICE_VISIBILITY.ORG_ONLY,
        user_type_ids: row.user_type_ids || [],
        supplier: row.supplier ?? SERVICE_SUPPLIER.ORG,
        supplier_user_type_ids: row.supplier_user_type_ids || [],
        resource_ids: row.resource_ids || [],
        flow_id: row.flow_id ?? null,
        flow_template: null,
        busy: false, error: null, info: null,
    });

    const closeEdit = () => { if (!edit.busy) setEdit(e => ({ ...e, open: false })); };

    // Each list offers only its own side's roles. The consumer list is always required: it is
    // what pressing confirm MAKES a member, so a service naming none can never be joined.
    const selfServe = edit.visibility !== SERVICE_VISIBILITY.INVITE_ONLY;
    const consumerTypes = types.filter(t => (t.side ?? USER_TYPE_SIDE.CONSUMER) === USER_TYPE_SIDE.CONSUMER);
    const supplierTypes = types.filter(t => t.side === USER_TYPE_SIDE.SUPPLIER);
    const sidesComplete =
        (edit.user_type_ids || []).length > 0 &&
        (edit.supplier !== SERVICE_SUPPLIER.USER_TYPES || (edit.supplier_user_type_ids || []).length > 0);

    const save = async () => {
        setEdit(e => ({ ...e, busy: true, error: null, info: null }));
        const payload = {
            name: edit.name,
            short_name: edit.short_name,
            description: edit.description,
            price: edit.price,
            visibility: edit.visibility,
            user_type_ids: edit.user_type_ids,
            supplier: edit.supplier,
            supplier_user_type_ids: edit.supplier_user_type_ids,
            resource_ids: edit.resource_ids,
            flow_id: edit.flow_id ?? null,
            flow_template: edit.flow_template ?? null,
        };
        try {
            if (edit.id) await axios.put("/service/edit", { id: edit.id, ...payload });
            else         await axios.post("/service/create", payload);
            setEdit(e => ({ ...e, busy: false, info: "Saved." }));
            await load();
            setTimeout(() => setEdit(e => ({ ...e, open: false })), 500);
        } catch (err) {
            setEdit(e => ({ ...e, busy: false, error: err.response?.data?.message || "Could not save." }));
        }
    };

    const setStatus = async (row, status) => {
        try {
            await axios.post("/service/status/set", { id: row.id, status });
            await load();
            notifySuccess(`${row.name} is now ${SERVICE_STATUS_LABEL[status].toLowerCase()}.`);
        } catch (err) {
            // Swallowing this to the console made a failed change look identical to one that did
            // nothing, which is exactly how the broken enum comparison stayed hidden. It also
            // carries the archive refusal, which the vendor genuinely needs to read.
            notifyError(err.response?.data?.message || "Could not change the service status.");
        }
    };

    const STATUS_SEVERITY = {
        [SERVICE_STATUS.ENABLED]:  "success",
        [SERVICE_STATUS.CLOSED]:   "warning",
        [SERVICE_STATUS.ARCHIVED]: "danger",
    };
    const statusBody = (row) => (
        <Tag severity={STATUS_SEVERITY[row.status] ?? "danger"}
            value={(SERVICE_STATUS_LABEL[row.status] ?? "unknown").toLowerCase()} />
    );
    const priceBody = (row) => row.price > 0 ? `HK$${parseFloat(row.price).toFixed(2)}` : "Free";
    // BOTH SIDES. The column named only the roles that BUY, so a two-sided service looked
    // identical to a one-sided one — Test Service C, M and N all read "Tester (Consumer)" and
    // nothing on this screen said who delivers them. Tinted the way Setup › User types tints the
    // two sides, so the same colour means the same thing on both screens. (Arfu, 16/09/2026)
    const typesBody = (row) => (
        <div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
            {(row.user_types || []).map(t => <Tag key={t.id} value={t.name} />)}
            {(row.user_types || []).length === 0 && <Tag severity="warning" value="none" />}
            {(row.supplier_user_types || []).map(t => (
                <Tag key={`s${t.id}`} severity="info" value={t.name} />
            ))}
        </div>
    );
    const actionsBody = (row) => {
        return (
            <div style={{ display: "flex", gap: 6, flexWrap: "nowrap" }}>
                <Button icon="pi pi-pencil" label="Edit" className="p-button-sm p-button-info" onClick={() => openEdit(row)} />
                {/* Amber when something is owed — the badge says WHAT is short, this says where
                    to go about it. */}
                <Button icon="pi pi-list" label="Details" onClick={() => openDetail(row)}
                    className={`p-button-sm ${row.details_missing > 0 ? "p-button-warning" : "p-button-secondary"}`} />
                <Button icon="pi pi-users" label="People" className="p-button-sm p-button-secondary" onClick={() => openRoster(row)} />

                {/* Three states, so no single "other" to flip to. Close and Reopen are the everyday
                    ones and stay inline, LABELLED: they were icon-only only because five labelled
                    buttons overflowed the row, and moving Archive out bought that space back.
                    `title`, NOT PrimeReact's `tooltip` prop — that needs primereact/tooltip loaded,
                    and without it the text rendered raw and unpositioned across the page. */}
                {row.status !== SERVICE_STATUS.ENABLED && (
                    <Button icon="pi pi-play" label="Reopen" className="p-button-sm p-button-success"
                        title="Reopen — anyone eligible can find and book it again."
                        onClick={() => setStatus(row, SERVICE_STATUS.ENABLED)} />
                )}
                {row.status === SERVICE_STATUS.ENABLED && (
                    <Button icon="pi pi-pause" label="Close" className="p-button-sm p-button-warning"
                        title="Close — stops new sign-ups and new bookings. People already on it keep access."
                        onClick={() => setStatus(row, SERVICE_STATUS.CLOSED)} />
                )}
                {/* Archive is rare and near-terminal, and as a bare red icon it said nothing at all
                    next to a bare amber one. Behind the overflow it is named, and its refusal
                    reason is readable — a disabled BUTTON fires no mouse events, so the old title
                    never showed in exactly the case that needed explaining. */}
                {row.status !== SERVICE_STATUS.ARCHIVED && (
                    <Button icon="pi pi-ellipsis-h" className="p-button-sm p-button-secondary"
                        aria-label="More actions" aria-haspopup
                        onClick={(e) => { setMenuRow(row); menuRef.current?.toggle(e); }} />
                )}
            </div>
        );
    };

    // Rebuilt for whichever row opened the menu. A blocked Archive keeps its place and says why,
    // rather than vanishing — a missing item reads as a bug, a disabled one reads as a rule.
    const rowMenu = () => {
        const onRoster = menuRow?.participants_count ?? 0;

        return [{
            label: onRoster > 0 ? `Archive — ${onRoster} still on the roster` : "Archive",
            icon: "pi pi-inbox",
            disabled: onRoster > 0,
            command: () => setStatus(menuRow, SERVICE_STATUS.ARCHIVED),
        }];
    };

    return (
        <div className="pt-fade-in">
            <Menu model={rowMenu()} popup ref={menuRef} />

            {/* Opened from the service form, which hid itself to make room. Whatever was typed is
                still in `edit`, so it comes back with the new flow already chosen. */}
            {flowModal && (
                <NewFlowModal
                    onClose={() => { setFlowModal(false); setEdit(e => ({ ...e, open: true })); }}
                    onCreated={async (flow) => {
                        setFlowModal(false);
                        await load();
                        setEdit(e => ({ ...e, open: true, flow_id: flow?.id ?? e.flow_id, flow_template: null }));
                    }}
                />
            )}
            <div style={{ display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8, marginBottom: 16 }}>
                <RefreshButton onClick={load} />
                <Button icon="pi pi-plus" label="New service" className="p-button-sm" onClick={openCreate}
                    disabled={types.length < 1} />
            </div>

            {/* A blocker that only states the rule leaves the reader hunting the sidebar for the
                screen that lifts it. New service is already disabled; this is the way out. */}
            {types.length < 1 && !loading && (
                <div className="pt-card" style={{ display: "flex", alignItems: "center", justifyContent: "space-between",
                    gap: 14, padding: "14px 16px", marginBottom: 14 }}>
                    <div>
                        <b>No user types yet.</b>
                        <div style={{ color: "var(--muted)", fontSize: 12.5, marginTop: 2 }}>
                            A service is offered to one or more user types, so at least one has to exist first.
                        </div>
                    </div>
                    <Button label="Create a user type" icon="pi pi-arrow-right" iconPos="right"
                        className="p-button-sm" onClick={() => onNavigate && onNavigate("UserType")}
                        disabled={!onNavigate} />
                </div>
            )}

            <div className="pt-card">
                {/* The table paginates only once there is something to page through — pager chrome
                    under "No services yet." reads as a broken table. */}
                {loading ? (
                    <div style={{ display: "flex", justifyContent: "center", padding: "40px 0" }}>
                        <ProgressSpinner style={{ width: "2.5rem", height: "2.5rem" }} />
                    </div>
                ) : (
                    <DataTable value={items} dataKey="id" paginator={items.length > 10} rows={10}
                        emptyMessage="No services yet.">
                        <Column field="name" header="Name" body={(r) => (
                            <div>
                                <div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
                                    <span>{r.name}</span>
                                    {/* The only place an unanswered required detail is visible.
                                        Nothing prompts for these — they live behind the Details
                                        dialog, one service at a time — so without the badge the
                                        org has to open every service to find the one that is short. */}
                                    {r.details_missing > 0 && (
                                        <Tag severity="warning" value={r.details_missing === 1 ? "1 detail needed" : `${r.details_missing} details needed`} />
                                    )}
                                </div>
                                {/* Support searches by this, so it has to be readable somewhere. */}
                                <div style={{ fontFamily: "var(--mono)", fontSize: 11, color: "var(--muted)", marginTop: 2 }}>{r.code}</div>
                            </div>
                        )} />
                        <Column header="User types" body={typesBody} />
                        <Column header="Price" body={priceBody} style={{ width: 120 }} />
                        <Column header="Status" body={statusBody} style={{ width: 120 }} />
                        <Column header="Reach" style={{ width: 120 }} body={(r) => (
                            <Tag severity={r.visibility === SERVICE_VISIBILITY.PUBLIC ? "success"
                                : (r.visibility === SERVICE_VISIBILITY.INVITE_ONLY ? "warning" : "info")}
                                value={(SERVICE_VISIBILITY_LABEL[r.visibility ?? SERVICE_VISIBILITY.ORG_ONLY]).toLowerCase()} />
                        )} />
                        <Column field="participants_count" header="Participants" style={{ width: 130 }} body={(r) => r.participants_count ?? 0} />
                        <Column header="Actions" body={actionsBody} style={{ width: 330 }} />
                    </DataTable>
                )}
            </div>

            <Dialog visible={roster.open} onHide={() => !roster.busy && setRoster(r => ({ ...r, open: false }))}
                header={`People — ${roster.service?.name || ""}`} style={{ width: "38rem" }}>
                {roster.busy && roster.rows.length === 0 ? (
                    <div style={{ color: "var(--muted)", fontSize: 13 }}>Loading…</div>
                ) : roster.rows.length === 0 ? (
                    <div style={{ color: "var(--muted)", fontSize: 13 }}>
                        Nobody has joined this service yet.
                    </div>
                ) : (
                    <DataTable value={roster.rows} dataKey="id" emptyMessage="Nobody yet.">
                        <Column field="name" header="Name" />
                        <Column field="email" header="Email" />
                        <Column header="Role" body={(r) => r.user_type
                            ? <Tag value={r.user_type} />
                            : <span style={{ color: "var(--muted)" }}>—</span>} />
                        {/* One roster, both halves — and only the consumers block archiving. */}
                        <Column header="Side" style={{ width: 110 }} body={(r) => (
                            <Tag severity={r.side === USER_TYPE_SIDE.SUPPLIER ? "info" : "success"}
                                value={(USER_TYPE_SIDE_LABEL[r.side ?? USER_TYPE_SIDE.CONSUMER]).toLowerCase()} />
                        )} />
                        <Column header="" style={{ width: 110 }} body={(r) => (
                            <Button icon="pi pi-times" label="Remove" className="p-button-sm p-button-danger"
                                disabled={roster.busy} onClick={() => removeParticipant(r.user_id)} />
                        )} />
                    </DataTable>
                )}
            </Dialog>

            <Dialog visible={detail.open} onHide={() => !detail.busy && setDetail(d => ({ ...d, open: false }))}
                header={`Details — ${detail.service?.name || ""}`} style={{ width: "34rem" }}>
                <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
                    {/* The schema comes from this service's own Service fields. With none defined the
                        form renders nothing at all, which looks like a broken dialog rather than an
                        empty one — and there is no Save button either, so there is no hint why. */}
                    {!detail.busy && detail.schema.length === 0 ? (
                        <div style={{ color: "var(--muted)", fontSize: 13, lineHeight: 1.6 }}>
                            No fields are defined for <b>{detail.service?.name}</b> yet. Add them under
                            <b> Setup › Service fields</b> — they decide what this service asks at booking
                            time, and this dialog is where the answers are recorded.
                        </div>
                    ) : (
                        <DetailForm schema={detail.schema} info={detail.info} disabled={detail.busy}
                            onChange={(info) => setDetail(d => ({ ...d, info }))} />
                    )}
                    <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
                        <Button label="Close" className="p-button-sm p-button-secondary" disabled={detail.busy}
                            onClick={() => setDetail(d => ({ ...d, open: false }))} />
                        {detail.schema.length > 0 && (
                            <Button label={detail.busy ? "Saving…" : "Save"} className="p-button-sm"
                                loading={detail.busy} disabled={detail.busy} onClick={saveDetail} />
                        )}
                    </div>
                </div>
            </Dialog>

            <Dialog visible={edit.open} onHide={closeEdit} closable={!edit.busy} closeOnEscape={!edit.busy}
                header={edit.id ? "Edit service" : "New service"} style={{ width: "30rem" }}>
                <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
                    <div>
                        <label htmlFor="sv-name" className="form-label">Name</label>
                        <InputText id="sv-name" value={edit.name} onChange={(e) => setEdit(s => ({ ...s, name: e.target.value }))}
                            placeholder="e.g. Bridal gown fitting" disabled={edit.busy} autoFocus />
                    </div>
                    <div>
                        {/* THE ORG WRITES THIS, and it is required (17/09/2026): it is what the
                            service is called wherever its full name will not fit — a chat pill, a
                            phone row. Not derived from the name, because "Test Ser" would look
                            enough like an answer to be left alone for ever. */}
                        <label htmlFor="sv-short" className="form-label">Short name</label>
                        <InputText id="sv-short" value={edit.short_name}
                            onChange={(e) => setEdit(s => ({ ...s, short_name: e.target.value }))}
                            placeholder="e.g. FITTING" maxLength={8} disabled={edit.busy}
                            style={{ fontFamily: "var(--mono)", textTransform: "none" }} />
                        <div style={{ fontSize: 11.5, color: "var(--muted)", marginTop: 4 }}>
                            4–8 characters, and different from your other services'. Shown where the full name will not fit.
                        </div>
                    </div>
                    <div>
                        <label htmlFor="sv-desc" className="form-label">Description (optional)</label>
                        <InputTextarea id="sv-desc" rows={3} value={edit.description}
                            onChange={(e) => setEdit(s => ({ ...s, description: e.target.value }))}
                            disabled={edit.busy} style={{ width: "100%" }} />
                    </div>
                    <div>
                        <label htmlFor="sv-price" className="form-label">Price (HK$)</label>
                        <InputNumber id="sv-price" value={edit.price} onValueChange={(e) => setEdit(s => ({ ...s, price: e.value ?? 0 }))}
                            mode="decimal" minFractionDigits={0} maxFractionDigits={2} min={0}
                            disabled={edit.busy} style={{ width: "100%" }} />
                    </div>
                    {/* Owner decision 31/08/2026: the FLOW decides whether a booking needs a time
                        and when it is picked, so a service names the flow that sells it. Left
                        empty, the customer gets the direct booking screen — a service must never
                        become unsellable for want of a flow. */}
                    <div>
                        <label htmlFor="sv-flow" className="form-label">Sold through</label>
                        <Dropdown id="sv-flow" value={edit.flow_id ?? null} showClear
                            /* One flow, one service: a flow already selling something else is not
                               offered, or binding it would make "this flow sells X" a lie on one of
                               the two cards. The one bound to THIS service stays, obviously. */
                            options={flows
                                .filter(f => !f.service_id || f.service_id === edit.id)
                                .map(f => ({
                                    label: flowLabel(f) + (f.status === FLOW_STATUS.PUBLISHED ? "" : " — not published"),
                                    value: f.id,
                                }))}
                            onChange={(e) => setEdit(s => ({ ...s, flow_id: e.value ?? null, flow_template: e.value ? null : s.flow_template }))}
                            placeholder={flows.length ? "Choose a chatflow…" : "You have no chatflows yet"}
                            disabled={edit.busy} style={{ width: "100%" }} />

                        {/* Offered whenever nothing is bound — on a NEW service and on an existing
                            one alike. Gating this to create left every service made before today
                            unbookable with no way forward, which is exactly the dead end templates
                            exist to close. Regenerating over a flow already chosen is the case
                            worth avoiding, and that is what the flow_id check covers. */}
                        {!edit.flow_id && (
                            <div style={{ marginTop: 8 }}>
                                <div style={{ fontSize: 12, color: "var(--muted)", marginBottom: 6 }}>Or generate one:</div>
                                <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                                    {[
                                        ["booking_with_time", "Booking with a time", "Asks for a time, then books it."],
                                        ["enquiry_no_time", "Enquiry only", "Opens the chat. Books nothing."],
                                    ].map(([key, label, hint]) => (
                                        <button key={key} type="button" disabled={edit.busy} title={hint}
                                            className={`p-button p-button-sm ${edit.flow_template === key ? "" : "p-button-secondary"}`}
                                            onClick={() => setEdit(s => ({ ...s, flow_template: s.flow_template === key ? null : key }))}>
                                            {label}
                                        </button>
                                    ))}
                                    <button type="button" disabled={edit.busy} className="p-button p-button-sm p-button-secondary"
                                        onClick={() => { setEdit(s => ({ ...s, open: false })); setFlowModal(true); }}>
                                        Build one…
                                    </button>
                                </div>
                            </div>
                        )}

                        <small style={{ color: "var(--muted)" }}>
                            {edit.flow_template
                                ? "A new chatflow will be created and published with this service, named after it. Edit it later under Growth › Chatflows."
                                : "The chat that sells this service — it decides what is asked and whether a time is picked at all. Only a PUBLISHED flow reaches customers, and a service with none cannot be booked."}
                        </small>
                    </div>
                    <div>
                        <label htmlFor="sv-resources" className="form-label">Resources</label>
                        <MultiSelect id="sv-resources" value={edit.resource_ids} onChange={(e) => setEdit(s => ({ ...s, resource_ids: e.value }))}
                            options={resources} optionLabel="name" optionValue="id" placeholder="Which resources can deliver this?"
                            display="chip" disabled={edit.busy} style={{ width: "100%" }} />
                        {/* "No available options" is a dead end when the screen that makes them is
                            somewhere else entirely. Resources live under Availability. */}
                        <small style={{ color: "var(--muted)" }}>
                            {resources.length === 0
                                ? "You have no resources yet — they are created under Setup › Availability, which is also where their weekly hours live. A service with none cannot be scheduled, so add one before taking bookings."
                                : "Candidates — a booking is bound to whichever is free. Listing several lets the service take several bookings at the same time. None means it cannot be scheduled."}
                        </small>
                    </div>
                    {/* Two sides, asked separately: who may BOOK it, and who DELIVERS it. Each
                        role list only offers roles from its own side, so a supplier role can
                        never end up gating who books — the server refuses that anyway. */}
                    <div>
                        <label htmlFor="sv-visibility" className="form-label">Who can find this</label>
                        <Dropdown id="sv-visibility" value={edit.visibility} disabled={edit.busy} style={{ width: "100%" }}
                            options={[
                                { label: "Public — any registered user", value: SERVICE_VISIBILITY.PUBLIC },
                                { label: "Org-only — members of your organization", value: SERVICE_VISIBILITY.ORG_ONLY },
                                { label: "Invite-only — only people you send a link to", value: SERVICE_VISIBILITY.INVITE_ONLY },
                            ]}
                            onChange={(e) => setEdit(s => ({ ...s, visibility: e.value }))} />
                        {edit.visibility === SERVICE_VISIBILITY.INVITE_ONLY && (
                            <small style={{ color: "var(--muted)" }}>
                                Nobody finds this on their own. Issue a join link pointing at this service
                                under Setup › Join links — following it enrols them and grants the role.
                            </small>
                        )}
                    </div>
                    <div>
                        <label htmlFor="sv-types" className="form-label">Customers join as</label>
                        {/* ONE consumer role, not a list: the customer does not pick their own
                            category, so a choice here would be a question with no right answer. */}
                        <Dropdown id="sv-types" value={edit.user_type_ids?.[0] ?? null}
                            onChange={(e) => setEdit(s => ({ ...s, user_type_ids: e.value ? [e.value] : [] }))}
                            options={consumerTypes} optionLabel="name" optionValue="id"
                            placeholder="The role a customer joins as" showClear
                            disabled={edit.busy} style={{ width: "100%" }} />
                        <small style={{ color: "var(--muted)" }}>
                            {consumerTypes.length === 0
                                ? "You have no consumer roles yet — they are created under Setup › User types."
                                : selfServe
                                    ? "The role someone takes to BOOK this. It must be enabled and not restricted, or nobody can join. Roles that deliver instead go in the field below."
                                    : "The role an invited customer becomes. A restricted role is fine here — the join link carries the grant."}
                        </small>
                    </div>

                    <div>
                        <label htmlFor="sv-supplier" className="form-label">Helpers join as</label>
                        <Dropdown id="sv-supplier" value={edit.supplier} disabled={edit.busy} style={{ width: "100%" }}
                            options={[
                                { label: "Nobody — the organization delivers it", value: SERVICE_SUPPLIER.ORG },
                                { label: "Specific user types", value: SERVICE_SUPPLIER.USER_TYPES },
                            ]}
                            onChange={(e) => setEdit(s => ({ ...s, supplier: e.value }))} />
                        <small style={{ color: "var(--muted)" }}>
                            {edit.supplier === SERVICE_SUPPLIER.ORG
                                ? "Your organization fulfils this; no member helps deliver it. Pick specific types to let people join on the delivering side."
                                : "Members can join under these roles to help deliver, and they see the service whether or not the booking side includes them. Mark a role restricted under Setup › User types if only you should hand it out."}
                        </small>
                    </div>
                    {edit.supplier === SERVICE_SUPPLIER.USER_TYPES && (
                        <div>
                            <label htmlFor="sv-supplier-types" className="form-label">Supplier types</label>
                            <MultiSelect id="sv-supplier-types" value={edit.supplier_user_type_ids}
                                onChange={(e) => setEdit(s => ({ ...s, supplier_user_type_ids: e.value }))}
                                options={supplierTypes} optionLabel="name" optionValue="id" placeholder="The roles that can help deliver this"
                                display="chip" disabled={edit.busy} style={{ width: "100%" }} />
                            <small style={{ color: "var(--muted)" }}>
                                {supplierTypes.length === 0
                                    ? "You have no supplier roles yet — create one under Setup › User types and set its side to Supplier."
                                    : "More than one is allowed: a job may take, say, a Photographer and an Assistant."}
                            </small>
                        </div>
                    )}
                    {edit.error && <Message severity="error" text={edit.error} />}
                    {edit.info && <Message severity="success" text={edit.info} />}
                    <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
                        <Button label="Cancel" className="p-button-sm p-button-secondary" onClick={closeEdit} disabled={edit.busy} />
                        <Button label={edit.busy ? "Saving…" : "Save"} icon={edit.busy ? null : "pi pi-check"}
                            loading={edit.busy} className="p-button-sm" onClick={save}
                            // The short name is required and bounded, so the button says so before
                            // the server has to.
                            disabled={edit.busy || !edit.name || (edit.short_name || "").trim().length < 4
                                || (edit.short_name || "").trim().length > 8 || !sidesComplete} />
                    </div>
                </div>
            </Dialog>
        </div>
    );
};

export default Service;
