import { useEffect, useRef, useState } from "react";
import type { ClipboardEvent, KeyboardEvent } from "react";

const CODE_LENGTH = 6;

interface CodeInputProps {
    autoFocus?: boolean;
    disabled?: boolean;
    label: string;
    length?: number;
    onChange: (code: string) => void;
    onComplete?: (code: string) => void;
    value: string;
}

const digitsOnly = (value: string) => value.replace(/\D/g, "");

const CodeInput = ({
    autoFocus = false,
    disabled = false,
    label,
    length = CODE_LENGTH,
    onChange,
    onComplete,
    value,
}: CodeInputProps) => {
    const inputs = useRef<Array<HTMLInputElement | null>>([]);
    const spread = (code: string) =>
        Array.from({ length }, (_, index) => code[index] ?? "");

    // Boxes are held here rather than derived from `value` so that clearing a
    // box in the middle leaves a hole instead of shifting the digits after it
    // to the left. `value` is the holes-removed code the parent submits.
    const [boxes, setBoxes] = useState(() => spread(value));

    useEffect(() => {
        if (value !== boxes.join("")) {
            setBoxes(spread(value));
        }
    }, [value]);

    const focusBox = (index: number) => {
        inputs.current[Math.min(Math.max(index, 0), length - 1)]?.focus();
    };

    const commit = (next: string[], focusIndex: number) => {
        const code = next.join("");

        setBoxes(next);
        onChange(code);
        focusBox(focusIndex);

        if (code.length === length) {
            onComplete?.(code);
        }
    };

    // Writing into a box replaces that digit; a multi-digit value (a paste or
    // an autofilled one-time code) spills into the boxes that follow it.
    const handleChange = (index: number, entered: string) => {
        const digits = digitsOnly(entered);

        if (!digits) {
            return;
        }

        const next = boxes.slice();

        digits
            .split("")
            .slice(0, length - index)
            .forEach((digit, offset) => {
                next[index + offset] = digit;
            });

        commit(next, index + digits.length);
    };

    const handleKeyDown = (index: number, event: KeyboardEvent) => {
        if (event.key === "Backspace") {
            event.preventDefault();

            // Backspacing an empty box clears the previous one instead.
            const target = boxes[index] ? index : index - 1;

            if (target < 0) {
                return;
            }

            const next = boxes.slice();
            next[target] = "";

            commit(next, target);

            return;
        }

        if (event.key === "ArrowLeft") {
            event.preventDefault();
            focusBox(index - 1);
        }

        if (event.key === "ArrowRight") {
            event.preventDefault();
            focusBox(index + 1);
        }
    };

    const handlePaste = (event: ClipboardEvent) => {
        event.preventDefault();

        const digits = digitsOnly(event.clipboardData.getData("text")).slice(
            0,
            length,
        );

        if (digits) {
            commit(spread(digits), digits.length);
        }
    };

    return (
        <div
            aria-label={label}
            className="code-input"
            onPaste={handlePaste}
            role="group"
        >
            {boxes.map((digit, index) => (
                <input
                    aria-label={`${label}, digit ${index + 1}`}
                    autoComplete={index === 0 ? "one-time-code" : "off"}
                    autoFocus={autoFocus && index === 0}
                    className={`code-input__box${digit ? " code-input__box--filled" : ""}`}
                    disabled={disabled}
                    inputMode="numeric"
                    key={index}
                    onChange={(event) =>
                        handleChange(index, event.target.value)
                    }
                    onFocus={(event) => event.target.select()}
                    onKeyDown={(event) => handleKeyDown(index, event)}
                    ref={(element) => {
                        inputs.current[index] = element;
                    }}
                    type="text"
                    value={digit}
                />
            ))}
        </div>
    );
};

export { CODE_LENGTH };
export default CodeInput;
