import '@xyflow/react/dist/style.css';
import {
    addEdge, Background, BackgroundVariant, Controls, Handle, MiniMap, Position, ReactFlow,
    ReactFlowProvider, useEdgesState, useNodesInitialized, useNodesState, useReactFlow, type Connection, type Edge,
    type NodeProps,
} from '@xyflow/react';
import {
    AlertTriangle, ArrowLeft, Check, CircleHelp, Clock3, GitBranch, LoaderCircle, Play, Plus,
    Save, Settings2, Sparkles, Square, Trash2, Workflow, X, Zap,
} from 'lucide-react';
import { useCallback, useEffect, useRef, useState, type DragEvent } from 'react';
import { Link, useParams } from 'react-router-dom';
import { useApp } from '../context/AppContext';
import { ApiError } from '../services/api';
import { contactsApi, flowsApi } from '../services/domain';
import { aiAgentsApi } from '../services/ai';
import type { AIAgent, Contact, CustomField, Flow, FlowExecution, FlowGraph, Tag } from '../types';
import { getPublishedFlows, makeNode, NODE_LIBRARY, TITLES, toCanvas, toGraph, updateNodeConfig, type BuilderNode } from './flowGraph';

const OPERATORS = ['equals', 'not_equals', 'contains', 'not_contains', 'starts_with', 'ends_with', 'greater_than', 'greater_than_or_equal', 'less_than', 'less_than_or_equal', 'is_empty', 'is_not_empty', 'in', 'not_in'];
const CONTACT_FIELDS = [
    { value: 'contact.first_name', label: 'First name', type: 'text' },
    { value: 'contact.last_name', label: 'Last name', type: 'text' },
    { value: 'contact.email', label: 'Email', type: 'email' },
    { value: 'contact.phone', label: 'Phone', type: 'phone' },
    { value: 'contact.language', label: 'Language', type: 'text' },
    { value: 'contact.timezone', label: 'Timezone', type: 'text' },
];
function AutomationNode({ data, selected }: NodeProps<BuilderNode>) {
    const Icon = NODE_LIBRARY.flatMap((group) => group.types).find((node) => node.type === data.type)?.icon ?? Zap;
    const description = data.type === 'send_message' ? String(data.config.text ?? 'Write a message to your customer')
        : data.type === 'condition' ? 'Route contacts based on rules'
        : data.type === 'delay' ? `Wait ${data.config.amount ?? 1} ${data.config.unit ?? 'minutes'}`
        : data.type === 'stop' ? 'Stop this automation'
        : data.type === 'http_request' ? `${data.config.method ?? 'GET'} · ${data.config.url ?? 'Configure endpoint'}`
        : data.type === 'ai_agent' ? `${data.config.agent_name ?? 'Select an AI agent'} · Send response: ${data.config.send_response === false ? 'No' : 'Yes'}`
        : data.name || 'Configure this step';
    return (
        <div className={`flow-node-card ${selected ? 'selected' : ''} ${data.invalid ? 'invalid' : ''} node-${data.type}`}>
            {data.type !== 'start' && <Handle type="target" position={Position.Top} id="default" />}
            <div className="flow-node-top"><span className="flow-node-icon"><Icon size={15} /></span><span className="flow-node-title">{TITLES[data.type] ?? data.type}</span>{data.invalid && <span className="node-warning" title="Validation warning"><AlertTriangle size={15} /></span>}</div>
            <p>{description.length > 54 ? `${description.slice(0, 54)}…` : description}</p>
            {data.type === 'condition' ? <div className="condition-handles"><span>YES</span><span>NO</span></div> : null}
            {data.type === 'condition' ? <>
                <Handle type="source" position={Position.Bottom} id="yes" style={{ left: '28%' }} />
                <Handle type="source" position={Position.Bottom} id="no" style={{ left: '72%' }} />
            </> : data.type !== 'stop' && <Handle type="source" position={Position.Bottom} id="default" />}
        </div>
    );
}
const nodeTypes = { automation: AutomationNode };

function FlowBuilderCanvas({ flow, initialGraph, onFlowChange }: { flow: Flow; initialGraph: FlowGraph; onFlowChange: (flow: Flow) => void }) {
    const { current, can } = useApp();
    const { screenToFlowPosition, fitView } = useReactFlow();
    const nodesInitialized = useNodesInitialized();
    const [nodes, setNodes, onNodesChange] = useNodesState<BuilderNode>([]);
    const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
    const [selectedId, setSelectedId] = useState<string | null>(null);
    const [tags, setTags] = useState<Tag[]>([]);
    const [fields, setFields] = useState<CustomField[]>([]);
    const [otherFlows, setOtherFlows] = useState<Flow[]>([]);
    const [agents, setAgents] = useState<AIAgent[]>([]);
    const [dirty, setDirty] = useState(false);
    const [saving, setSaving] = useState(false);
    const [publishing, setPublishing] = useState(false);
    const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);
    const [validationErrors, setValidationErrors] = useState<string[]>([]);
    const [notice, setNotice] = useState('');
    const [error, setError] = useState('');
    const [showTest, setShowTest] = useState(false);
    const [contacts, setContacts] = useState<Contact[]>([]);
    const [testContactId, setTestContactId] = useState('');
    const [testBusy, setTestBusy] = useState(false);
    const [executions, setExecutions] = useState<FlowExecution[]>([]);
    const [executionDetail, setExecutionDetail] = useState<FlowExecution | null>(null);
    const [showExecutions, setShowExecutions] = useState(false);
    const revision = useRef(0);
    const graphRef = useRef<FlowGraph>({ nodes: [], edges: [] });
    const baseline = useRef('');
    const hydrated = useRef(false);
    const needsInitialFit = useRef(false);
    const queueRef = useRef<Promise<void>>(Promise.resolve());
    const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
    const selectedNode = nodes.find((node) => node.id === selectedId) ?? null;

    useEffect(() => {
        if (!current) return;
        let active = true;
        void Promise.all([
            contactsApi.tags(current.workspace.id),
            contactsApi.fields(current.workspace.id),
            flowsApi.list(current.workspace.id),
            can('ai.view') ? aiAgentsApi.list(current.workspace.id) : Promise.resolve([] as AIAgent[]),
        ]).then(([availableTags, availableFields, flowPage, availableAgents]) => {
            if (!active) return;
            setTags(availableTags);
            setFields(availableFields);
            setOtherFlows(getPublishedFlows(flowPage.items, flow.id));
            setAgents(availableAgents.filter((agent) => agent.status === 'active'));
        }).catch((cause: unknown) => {
            if (active) setError(cause instanceof Error ? `Could not load flow settings: ${cause.message}` : 'Could not load flow settings.');
        });
        return () => { active = false; };
    }, [current?.workspace.id, flow.id, can]);

    useEffect(() => {
        const canvas = toCanvas(initialGraph);
        const graph = toGraph(canvas.nodes, canvas.edges);
        baseline.current = JSON.stringify(graph);
        graphRef.current = graph;
        hydrated.current = true;
        needsInitialFit.current = true;
        setNodes(canvas.nodes);
        setEdges(canvas.edges);
    }, [initialGraph, setEdges, setNodes, fitView]);

    useEffect(() => {
        if (!needsInitialFit.current || !nodesInitialized) return;
        needsInitialFit.current = false;
        const frame = requestAnimationFrame(() => { void fitView({ padding: 0.2, duration: 180 }); });
        return () => cancelAnimationFrame(frame);
    }, [nodesInitialized, fitView]);

    useEffect(() => {
        if (!dirty) return;
        const warning = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = ''; };
        window.addEventListener('beforeunload', warning);
        return () => window.removeEventListener('beforeunload', warning);
    }, [dirty]);

    useEffect(() => {
        const graph = toGraph(nodes, edges);
        graphRef.current = graph;
        if (hydrated.current && JSON.stringify(graph) !== baseline.current) {
            setDirty(true);
            revision.current++;
        }
    }, [nodes, edges]);

    const persist = useCallback(async (): Promise<void> => {
        if (!current) return;
        const snapshot = graphRef.current;
        const saveRevision = revision.current;
        setSaving(true);
        const queued = queueRef.current.catch(() => undefined).then(async () => {
            await flowsApi.saveGraph(current.workspace.id, flow.id, snapshot);
            baseline.current = JSON.stringify(snapshot);
            if (revision.current === saveRevision) {
                setDirty(false);
                setLastSavedAt(new Date());
                setValidationErrors([]);
                setError('');
            }
        });
        queueRef.current = queued;
        try {
            await queued;
        } catch (cause) {
            setError(cause instanceof Error ? cause.message : 'Draft could not be saved.');
            throw cause;
        } finally {
            if (revision.current === saveRevision) setSaving(false);
        }
        return;
    }, [current?.workspace.id, flow.id]);

    useEffect(() => {
        if (!dirty) return;
        if (saveTimer.current) clearTimeout(saveTimer.current);
        saveTimer.current = setTimeout(() => { void persist().catch(() => undefined); }, 1400);
        return () => { if (saveTimer.current) clearTimeout(saveTimer.current); };
    }, [dirty, nodes, edges, persist]);

    const onConnect = useCallback((connection: Connection) => {
        const source = nodes.find((node) => node.id === connection.source);
        if (!source || !connection.target) return;
        const handle = source.data.type === 'condition' ? connection.sourceHandle : 'default';
        const edge: Edge = {
            ...connection, id: crypto.randomUUID(), sourceHandle: handle ?? 'default',
            targetHandle: connection.targetHandle ?? 'default', type: 'smoothstep',
            label: handle === 'yes' ? 'YES' : handle === 'no' ? 'NO' : undefined,
        };
        setEdges((currentEdges) => addEdge(edge, currentEdges));
        revision.current++;
        setValidationErrors([]);
    }, [nodes, setEdges]);

    const addNode = useCallback((type: string, position?: { x: number; y: number }) => {
        if (!can('flows.update')) return;
        if (type === 'start' && nodes.some((node) => node.data.type === 'start')) {
            setError('A flow can only have one Start node.');
            return;
        }
        const node = makeNode(type, position ?? { x: 180 + nodes.length % 3 * 260, y: 100 + Math.floor(nodes.length / 3) * 150 });
        setNodes((currentNodes) => [...currentNodes, node]);
        setSelectedId(node.id);
        revision.current++;
    }, [can, nodes, setNodes]);

    function onDrop(event: DragEvent) {
        event.preventDefault();
        if (!can('flows.update')) return;
        const type = event.dataTransfer.getData('application/relay-node');
        if (type) addNode(type, screenToFlowPosition({ x: event.clientX, y: event.clientY }));
    }

    function updateConfig(key: string, value: unknown) {
        if (!selectedNode) return;
        setNodes((currentNodes) => currentNodes.map((node) => node.id === selectedNode.id
            ? updateNodeConfig(node, key, value)
            : node));
        revision.current++;
        setValidationErrors([]);
    }

    async function publish() {
        if (!current || publishing) return;
        setPublishing(true);
        setError('');
        try {
            await persist();
            const result = await flowsApi.publish(current.workspace.id, flow.id);
            setNotice(`Published as version v${result.version}.`);
            onFlowChange(result.flow);
            setValidationErrors([]);
        } catch (cause) {
            if (cause instanceof ApiError) {
                const entries = Object.entries(cause.fields);
                const invalidIndexes = new Set(entries.flatMap(([key]) => {
                    const match = /^nodes\.(\d+)/.exec(key);
                    return match ? [Number(match[1])] : [];
                }));
                const invalidUuids = new Set(entries.flatMap(([key, messages]) => {
                    if (key !== 'nodes') return [];
                    return messages.flatMap((message) => Array.from(message.matchAll(/\[([^\]]+)\]/g), (match) => match[1]));
                }));
                if (invalidIndexes.size || invalidUuids.size) {
                    setNodes((currentNodes) => currentNodes.map((node, index) => ({
                        ...node, data: { ...node.data, invalid: invalidIndexes.has(index) || invalidUuids.has(node.id) },
                    })));
                }
                const messages = entries.flatMap(([, errors]) => errors);
                setValidationErrors(messages.length ? messages : [cause.message]);
            } else setError(cause instanceof Error ? cause.message : 'Could not publish the flow.');
        } finally { setPublishing(false); }
    }

    async function openTest() {
        if (!current) return;
        try {
            setContacts((await contactsApi.list(current.workspace.id, { per_page: 100 })).items);
            setShowTest(true);
        } catch (cause) { setError(cause instanceof Error ? cause.message : 'Could not load contacts.'); }
    }

    async function runTest() {
        if (!current || !testContactId || testBusy) return;
        setTestBusy(true);
        try {
            await persist();
            const execution = await flowsApi.test(current.workspace.id, flow.id, { contact_id: Number(testContactId) });
            setShowTest(false);
            setExecutionDetail(execution);
            setNotice(`Test execution #${execution.id} queued.`);
        } catch (cause) { setError(cause instanceof Error ? cause.message : 'Could not start test.'); }
        finally { setTestBusy(false); }
    }

    async function openExecution(executionId: number) {
        if (!current) return;
        try {
            setExecutionDetail(await flowsApi.execution(current.workspace.id, executionId));
        } catch (cause) { setError(cause instanceof Error ? cause.message : 'Could not load execution.'); }
    }

    async function loadExecutions() {
        if (!current) return;
        try {
            setExecutions((await flowsApi.executions(current.workspace.id, flow.id)).items);
            setShowExecutions(true);
        } catch (cause) { setError(cause instanceof Error ? cause.message : 'Could not load executions.'); }
    }

    return (
        <div className="builder-page">
            <header className="builder-header">
                <Link to="/app/automation/flows" className="back-link"><ArrowLeft size={17} /><span>Flows</span></Link>
                <span className="builder-divider" />
                <div className="builder-flow-title"><Workflow size={17} /><b>{flow.name}</b><span className={`status-pill ${flow.status}`}><i />{flow.status}</span><span className="version-label">v{flow.version || 0}</span></div>
                <span className="builder-save-state">{saving ? <><LoaderCircle className="spin" size={14} /> Saving…</> : dirty ? 'Unsaved changes' : <><Check size={14} />{lastSavedAt ? `Saved ${lastSavedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : 'All changes saved'}</>}</span>
                <div className="builder-actions">
                    <button className="button button-quiet" onClick={() => void loadExecutions()}><GitBranch size={15} />Executions</button>
                    {can('flows.update') && <button className="button button-quiet" onClick={() => void openTest()}><Play size={15} />Test flow</button>}
                    {can('flows.update') && <button className="button button-outline" disabled={!dirty || saving} onClick={() => void persist().catch(() => undefined)}><Save size={15} />Save</button>}
                    {can('flows.publish') && <button className="button button-primary" disabled={publishing || saving} onClick={() => void publish()}>{publishing ? <LoaderCircle className="spin" size={15} /> : <Sparkles size={15} />}{publishing ? 'Publishing…' : 'Publish'}</button>}
                </div>
            </header>
            {(error || notice || validationErrors.length > 0) && <div className="builder-alerts">
                {error && <div role="alert" className="alert-error">{error}<button onClick={() => setError('')}>×</button></div>}
                {notice && <div className="alert-success">{notice}<button onClick={() => setNotice('')}>×</button></div>}
                {validationErrors.map((message, index) => <div role="alert" className="alert-error" key={`${message}-${index}`}><AlertTriangle size={15} />{message}<button onClick={() => setValidationErrors((items) => items.filter((_, itemIndex) => itemIndex !== index))}>×</button></div>)}
            </div>}
            <div className="builder-workspace">
                <aside className="node-library">
                    <div className="builder-panel-heading"><div><span className="eyebrow">FLOW DESIGN</span><b>Node library</b></div><CircleHelp size={15} /></div>
                    <div className="node-library-scroll">
                        {NODE_LIBRARY.map((group) => <section className="library-group" key={group.group}><div className="library-group-title">{group.group}</div>
                            {group.types.map(({ type, label, icon: Icon }) => <button key={type} className="library-item" draggable={can('flows.update')} disabled={!can('flows.update')} onDragStart={(event) => {
                                event.dataTransfer.setData('application/relay-node', type);
                                event.dataTransfer.effectAllowed = 'move';
                            }} onClick={() => addNode(type)}><span className={`library-icon icon-${type}`}><Icon size={15} /></span><span>{label}</span><Plus size={14} /></button>)}
                        </section>)}
                    </div>
                    <div className="library-tip"><span className="tip-icon"><Zap size={15} /></span><span><b>Tip</b><small>Drag a step onto the canvas or click to add it.</small></span></div>
                </aside>
                <section className="flow-canvas" onDrop={onDrop} onDragOver={(event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }}>
                    <div className="canvas-topline"><div><span className="canvas-live-dot" />Draft canvas</div><span>{nodes.length} steps <span>·</span> {edges.length} connections</span></div>
                    <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes}
                        onNodesChange={(changes) => { onNodesChange(changes); if (changes.some((change) => change.type === 'position' && change.dragging === false)) revision.current++; }}
                        onEdgesChange={(changes) => { onEdgesChange(changes); if (changes.some((change) => change.type !== 'select')) revision.current++; }}
                        onConnect={can('flows.update') ? onConnect : undefined} nodesDraggable={can('flows.update')}
                        nodesConnectable={can('flows.update')} onNodeClick={(_, node) => setSelectedId(node.id)}
                        onPaneClick={() => setSelectedId(null)}
                        onNodesDelete={(deleted) => { if (deleted.some((node) => node.id === selectedId)) setSelectedId(null); revision.current++; }}
                        onEdgesDelete={() => { revision.current++; }}
                        deleteKeyCode={can('flows.update') ? ['Backspace', 'Delete'] : null} fitView
                        defaultEdgeOptions={{ type: 'smoothstep', style: { stroke: '#b8b7c4', strokeWidth: 1.7 } }}
                        >
                        <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#d9d8e0" />
                        <Controls position="bottom-left" />
                        <MiniMap position="bottom-right" pannable zoomable nodeColor={(node) => node.data?.invalid ? '#ef6a6a' : '#7a69d9'} />
                    </ReactFlow>
                    {nodes.length === 0 && <div className="canvas-empty"><div className="canvas-empty-icon"><Workflow size={24} /></div><b>Start with a trigger</b><p>Click Start in the node library, then connect steps to build your flow.</p>{can('flows.update') && <button className="button button-outline" onClick={() => addNode('start')}><Plus size={15} /> Add Start node</button>}</div>}
                    <div className="canvas-hint"><span>⌘</span> + scroll to zoom <i /> Drag empty space to pan</div>
                </section>
                <aside className="node-settings">
                    <div className="builder-panel-heading"><div><span className="eyebrow">STEP SETTINGS</span><b>{selectedNode ? TITLES[selectedNode.data.type] : 'Select a step'}</b></div>{can('flows.update') && selectedNode && selectedNode.data.type !== 'start' && <button className="icon-button small danger-icon" title="Delete step" onClick={() => {
                        setNodes((items) => items.filter((item) => item.id !== selectedNode.id));
                        setEdges((items) => items.filter((edge) => edge.source !== selectedNode.id && edge.target !== selectedNode.id));
                        setSelectedId(null); revision.current++;
                    }}><Trash2 size={16} /></button>}</div>
                    {selectedNode ? <fieldset className="settings-fieldset" disabled={!can('flows.update')}><NodeSettings node={selectedNode} onChange={updateConfig} tags={tags} fields={fields} flows={otherFlows} agents={agents} currentFlow={flow} /></fieldset> : <div className="settings-empty"><Settings2 size={23} /><b>Nothing selected</b><p>Choose a step on the canvas to edit its settings.</p></div>}
                </aside>
            </div>
            {showTest && <div className="modal-backdrop"><div className="modal-card"><button className="modal-close" onClick={() => setShowTest(false)}>×</button><div className="modal-icon"><Play size={19} /></div><div className="eyebrow">FLOW TEST</div><h2>Test this automation</h2><p>Choose a contact to run through the current draft graph.</p><label>Contact<select value={testContactId} onChange={(event) => setTestContactId(event.target.value)}><option value="">Select a contact</option>{contacts.map((contact) => <option value={contact.id} key={contact.id}>{contact.full_name || contact.email || `Contact #${contact.id}`}</option>)}</select></label><div className="modal-actions"><button className="button button-quiet" onClick={() => setShowTest(false)}>Cancel</button><button className="button button-primary" disabled={!testContactId || testBusy} onClick={() => void runTest()}>{testBusy ? 'Starting…' : 'Run test'}<Play size={15} /></button></div></div></div>}
            {showExecutions && <div className="drawer-backdrop" onClick={() => setShowExecutions(false)}><aside className="execution-drawer" onClick={(event) => event.stopPropagation()}><div className="drawer-heading"><div><span className="eyebrow">ACTIVITY</span><h2>Recent executions</h2></div><button className="icon-button" onClick={() => setShowExecutions(false)}><X size={18} /></button></div>{executions.length ? executions.map((execution) => <button className="execution-row" key={execution.id} onClick={() => void openExecution(execution.id)}><span className={`execution-status ${execution.status}`} /><span><b>{execution.contact?.name ?? `Contact #${execution.id}`}</b><small>#{execution.id} · {execution.started_at ? new Date(execution.started_at).toLocaleString() : 'Queued'}</small></span><span className={`status-pill ${execution.status}`}>{execution.status}</span></button>) : <div className="empty-mini">No executions yet. Trigger a flow or run a test.</div>}</aside></div>}
            {executionDetail && <div className="drawer-backdrop" onClick={() => setExecutionDetail(null)}><aside className="execution-drawer" onClick={(event) => event.stopPropagation()}><div className="drawer-heading"><div><span className="eyebrow">EXECUTION #{executionDetail.id}</span><h2>{executionDetail.flow.name}</h2></div><button className="icon-button" onClick={() => setExecutionDetail(null)}><X size={18} /></button></div><div className="execution-meta"><span className={`status-pill ${executionDetail.status}`}>{executionDetail.status}</span><span>v{executionDetail.flow.version ?? flow.version}</span><span>{executionDetail.contact?.name ?? 'Test contact'}</span></div>            <div className="execution-timeline">{executionDetail.steps.map((step, index) => <div className={`timeline-step ${step.status}`} key={step.id}><span className="timeline-mark">{step.status === 'completed' ? <Check size={13} /> : step.status === 'failed' ? <X size={13} /> : index + 1}</span><div><b>{TITLES[step.node_type] ?? step.node_type}</b><small>{step.started_at ? new Date(step.started_at).toLocaleTimeString() : step.status}</small>{step.status === 'failed' && <p className="muted">{step.error_message || 'This step could not be completed.'}</p>}</div></div>)}</div></aside></div>}
        </div>
    );
}

function NodeSettings({ node, onChange, tags, fields, flows, agents, currentFlow }: {
    node: BuilderNode; onChange: (key: string, value: unknown) => void; tags: Tag[]; fields: CustomField[]; flows: Flow[]; agents: AIAgent[]; currentFlow: Flow;
}) {
    const config = node.data.config;
    const allFields = [...CONTACT_FIELDS.map(({ value, label, type }) => ({ value, label, type })), ...fields.map((field) => ({ value: `custom.${field.key}`, label: field.name, type: field.type }))];
    const [rawBody, setRawBody] = useState(JSON.stringify(config.body ?? {}, null, 2));
    const [bodyError, setBodyError] = useState('');
    useEffect(() => { setRawBody(JSON.stringify(config.body ?? {}, null, 2)); setBodyError(''); }, [node.id]);
    if (node.data.type === 'start') return <div className="settings-form"><div className="info-card"><span className="settings-icon"><Play size={16} /></span><span><b>Start node</b><small>Entry point for every flow execution. Connect it to the first step.</small></span></div><div className="field-note"><Check size={14} /> This flow needs exactly one Start node to publish.</div></div>;
    if (node.data.type === 'stop') return <div className="settings-form"><div className="info-card"><span className="settings-icon"><Square size={16} /></span><span><b>Stop this automation</b><small>Execution ends here. No additional steps will run.</small></span></div></div>;
    if (node.data.type === 'send_message') return <div className="settings-form">
        <label>Message text<textarea rows={7} value={String(config.text ?? '')} onChange={(event) => onChange('text', event.target.value)} placeholder="Write a helpful message…" /></label>
        <details className="variable-picker"><summary><Plus size={14} /> Insert variable</summary><div>{CONTACT_FIELDS.slice(0, 4).map((field) => <button key={field.value} onClick={() => onChange('text', `${String(config.text ?? '')}{{${field.value}}}`)}>{field.label}<code>{`{{${field.value}}}`}</code></button>)}{fields.map((field) => <button key={field.key} onClick={() => onChange('text', `${String(config.text ?? '')}{{custom.${field.key}}}`)}>{field.name}<code>{`{{custom.${field.key}}}`}</code></button>)}</div></details>
        <div className="preview-card"><span className="eyebrow">MESSAGE PREVIEW</span><p>{String(config.text || 'Your message will appear here…').replace(/\{\{contact.first_name\}\}/g, 'Alex').replace(/\{\{contact.last_name\}\}/g, 'Morgan').replace(/\{\{custom\.[\w-]+\}\}/g, 'Example')}</p></div>
    </div>;
    if (node.data.type === 'condition') {
        const rules = Array.isArray(config.rules) ? config.rules as Array<Record<string, unknown>> : [];
        const updateRule = (index: number, key: string, value: unknown) => onChange('rules', rules.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, [key]: value } : rule));
        return <div className="settings-form"><label>Match rules<select value={String(config.logic ?? 'and')} onChange={(event) => onChange('logic', event.target.value)}><option value="and">All rules (AND)</option><option value="or">Any rule (OR)</option></select></label>
            {rules.map((rule, index) => <div className="rule-editor" key={index}><div className="rule-head"><b>Rule {index + 1}</b>{rules.length > 1 && <button className="text-button danger-text" onClick={() => onChange('rules', rules.filter((_, ruleIndex) => ruleIndex !== index))}>Remove</button>}</div>
                <select value={String(rule.field ?? '')} onChange={(event) => updateRule(index, 'field', event.target.value)}>{allFields.map((field) => <option key={field.value} value={field.value}>{field.label}</option>)}</select>
                <select value={String(rule.operator ?? 'equals')} onChange={(event) => updateRule(index, 'operator', event.target.value)}>{OPERATORS.map((operator) => <option key={operator} value={operator}>{operator.replaceAll('_', ' ')}</option>)}</select>
                {!['is_empty', 'is_not_empty'].includes(String(rule.operator)) && (['in', 'not_in'].includes(String(rule.operator))
                    ? <input value={Array.isArray(rule.value) ? rule.value.join(', ') : ''} onChange={(event) => updateRule(index, 'value', event.target.value.split(',').map((value) => value.trim()).filter(Boolean))} placeholder="Comma-separated values" />
                    : <input value={String(rule.value ?? '')} onChange={(event) => updateRule(index, 'value', event.target.value)} placeholder="Value" />)}
            </div>)}
            <button className="button button-outline full-button" onClick={() => onChange('rules', [...rules, { field: allFields[0].value, operator: 'equals', value: '' }])}><Plus size={15} /> Add rule</button>
            <div className="condition-output"><span className="yes-output">YES</span><span className="no-output">NO</span></div>
        </div>;
    }
    if (node.data.type === 'delay') return <div className="settings-form"><div className="section-hint">Pause this flow for a period of time.</div><label>Wait<div className="inline-fields"><input type="number" min="1" value={Number(config.amount ?? 1)} onChange={(event) => onChange('amount', Number(event.target.value))} /><select value={String(config.unit ?? 'minutes')} onChange={(event) => onChange('unit', event.target.value)}>{['seconds', 'minutes', 'hours', 'days'].map((unit) => <option key={unit}>{unit}</option>)}</select></div></label><div className="field-note"><Clock3 size={14} /> This step resumes automatically after the delay.</div></div>;
    if (node.data.type === 'add_tag' || node.data.type === 'remove_tag') return <div className="settings-form"><div className="section-hint">{node.data.type === 'add_tag' ? 'Attach a tag to this contact.' : 'Remove a tag from this contact.'}</div><label>Select tag<select value={String(config.tag_id ?? '')} onChange={(event) => onChange('tag_id', Number(event.target.value) || null)}><option value="">Choose a tag…</option>{tags.map((tag) => <option value={tag.id} key={tag.id}>{tag.name}</option>)}</select></label>{tags.length === 0 && <div className="field-note">No tags in this workspace yet. Create one from Contacts.</div>}</div>;
    if (node.data.type === 'set_field' || node.data.type === 'clear_field') {
        const fieldValue = String(config.field ?? 'custom.');
        const field = allFields.find((option) => option.value === fieldValue);
        return <div className="settings-form"><div className="section-hint">{node.data.type === 'set_field' ? 'Update a contact or custom field.' : 'Clear a contact or custom field value.'}</div><label>Field<select value={fieldValue} onChange={(event) => onChange('field', event.target.value)}>{allFields.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>
            {node.data.type === 'set_field' && field && <label>Value{field.type === 'boolean' ? <select value={String(config.value ?? 'true')} onChange={(event) => onChange('value', event.target.value === 'true')}><option value="true">True</option><option value="false">False</option></select>
                : field.type === 'multi_select' ? <select multiple value={Array.isArray(config.value) ? config.value.map(String) : []} onChange={(event) => onChange('value', Array.from(event.target.selectedOptions, (option) => option.value))}>{(fields.find((item) => `custom.${item.key}` === field.value)?.options ?? []).map((option) => <option key={option}>{option}</option>)}</select>
                : field.type === 'select' ? <select value={String(config.value ?? '')} onChange={(event) => onChange('value', event.target.value)}><option value="">Choose…</option>{(fields.find((item) => `custom.${item.key}` === field.value)?.options ?? []).map((option) => <option key={option}>{option}</option>)}</select>
                : <input type={field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : field.type === 'email' ? 'email' : 'text'} value={String(config.value ?? '')} onChange={(event) => onChange('value', field.type === 'number' ? Number(event.target.value) : event.target.value)} />}</label>}
        </div>;
    }
    if (node.data.type === 'http_request') return <div className="settings-form"><div className="section-hint">Send a secure request to an allowlisted endpoint.</div><label>Method<select value={String(config.method ?? 'GET')} onChange={(event) => onChange('method', event.target.value)}>{['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map((method) => <option key={method}>{method}</option>)}</select></label><label>URL<input value={String(config.url ?? '')} onChange={(event) => onChange('url', event.target.value)} placeholder="https://api.example.com/endpoint" /></label><KeyValueEditor label="Headers" value={config.headers} onChange={(value) => onChange('headers', value)} /><KeyValueEditor label="Query parameters" value={config.query} onChange={(value) => onChange('query', value)} /><label>JSON body<textarea className="code-input" rows={6} value={rawBody} onChange={(event) => { setRawBody(event.target.value); try { onChange('body', JSON.parse(event.target.value)); setBodyError(''); } catch { setBodyError('Enter valid JSON before saving or publishing this node.'); } }} spellCheck={false} /></label>{bodyError && <div role="alert" className="alert-error">{bodyError}</div>}<label>Timeout (seconds)<input type="number" min="1" max="30" value={Number(config.timeout ?? 10)} onChange={(event) => onChange('timeout', Number(event.target.value))} /></label><div className="field-note"><AlertTriangle size={14} /> The server blocks private networks and non-allowlisted hosts.</div></div>;
    if (node.data.type === 'start_flow') return <div className="settings-form"><div className="section-hint">Start another published flow for this contact.</div><label>Published flow<select value={String(config.flow_id ?? '')} onChange={(event) => onChange('flow_id', Number(event.target.value) || null)}><option value="">Choose a flow…</option>{flows.map((item) => <option value={item.id} key={item.id}>{item.name}{item.id === currentFlow.id ? ' (current)' : ''}</option>)}</select></label>{flows.length === 0 && <div className="field-note">Publish another flow first. Recursive calls are blocked by the runtime.</div>}</div>;
    if (node.data.type === 'ai_agent') return <div className="settings-form">
        <div className="section-hint">Run an active workspace AI agent using its attached knowledge and registered tools.</div>
        <label>AI Agent *<select value={String(config.agent_id ?? '')} onChange={(event) => {
            const selected = agents.find((agent) => agent.id === Number(event.target.value));
            onChange('agent_id', selected?.id ?? null);
            onChange('agent_name', selected?.name ?? '');
        }}><option value="">Choose an agent…</option>{agents.map((agent) => <option value={agent.id} key={agent.id}>{agent.name}</option>)}</select></label>
        {agents.length === 0 && <div className="field-note"><AlertTriangle size={14} /> No active AI agents in this workspace.</div>}
        <label className="ai-node-toggle"><input type="checkbox" checked={config.send_response !== false} onChange={(event) => onChange('send_response', event.target.checked)} /> Send response to customer</label>
        <label>Save response as<input value={String(config.save_response_as ?? '')} onChange={(event) => onChange('save_response_as', event.target.value)} placeholder="ai_response" /></label>
        {!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(String(config.save_response_as ?? '')) && <div role="alert" className="field-note"><AlertTriangle size={14} /> Use a simple variable name, such as ai_response.</div>}
        <div className="field-note">Later flow nodes can read this with <code>{`{{var.${String(config.save_response_as || 'ai_response')}}}`}</code>.</div>
    </div>;
    return <div className="settings-form"><div className="section-hint">This step has no additional settings.</div></div>;
}

function KeyValueEditor({ label, value, onChange }: { label: string; value: unknown; onChange: (value: Record<string, string>) => void }) {
    const entries = typeof value === 'object' && value !== null ? Object.entries(value as Record<string, unknown>) : [];
    function asStringRecord(rows: Array<[string, unknown]>): Record<string, string> {
        return rows.reduce<Record<string, string>>((result, [key, entryValue]) => {
            if (key) result[key] = String(entryValue);
            return result;
        }, {});
    }
    function setAt(index: number, key: string, next: string) {
        const updated = [...entries];
        updated[index] = key === 'key' ? [next, String(updated[index]?.[1] ?? '')] : [String(updated[index]?.[0] ?? ''), next];
        onChange(asStringRecord(updated));
    }
    return <div className="kv-editor"><div className="kv-heading"><b>{label}</b><button className="text-button" onClick={() => onChange({ ...Object.fromEntries(entries.map(([key, val]) => [key, String(val)])), '': '' })}><Plus size={13} /> Add</button></div>
        {entries.map(([key, val], index) => <div className="kv-row" key={`${index}-${key}`}><input value={key} placeholder="Key" onChange={(event) => setAt(index, 'key', event.target.value)} /><input value={String(val)} placeholder="Value" onChange={(event) => setAt(index, 'value', event.target.value)} /><button className="icon-button small" aria-label={`Remove ${label} row`} onClick={() => onChange(asStringRecord(entries.filter((_, rowIndex) => rowIndex !== index)))}><X size={14} /></button></div>)}
        {entries.length === 0 && <small className="muted">No {label.toLowerCase()} added.</small>}
    </div>;
}

export function FlowBuilderPage() {
    const { id } = useParams();
    const { current } = useApp();
    const [flow, setFlow] = useState<Flow | null>(null);
    const [initialGraph, setInitialGraph] = useState<FlowGraph | null>(null);
    const [error, setError] = useState('');
    useEffect(() => {
        if (!current || !id) return;
        let active = true;
        void Promise.all([flowsApi.get(current.workspace.id, Number(id)), flowsApi.graph(current.workspace.id, Number(id))])
            .then(([loadedFlow, graph]) => { if (active) { setFlow(loadedFlow); setInitialGraph(graph); } })
            .catch((cause: Error) => { if (active) setError(cause.message); });
        return () => { active = false; };
    }, [current?.workspace.id, id]);
    if (error) return <div className="builder-loading"><div role="alert" className="alert-error">{error}</div><Link className="button button-outline" to="/app/automation/flows">Back to flows</Link></div>;
    if (!flow || !initialGraph) return <div className="builder-loading"><LoaderCircle className="spin" size={24} /><span>Opening flow builder…</span></div>;
    return <ReactFlowProvider><FlowBuilderCanvas key={`${flow.id}-${current?.workspace.id}`} flow={flow} initialGraph={initialGraph} onFlowChange={setFlow} /></ReactFlowProvider>;
}
