import { debounce, isEmpty } from 'lodash';
import * as React from 'react';
import { useCallback, useRef, useState } from 'react';
import Icon from './Icon';
import ListItem from './ListItem';

// 1. Group related constants
const CONFIG = {
  HOST: '',
  LOG: false,
} as const;

const STYLES = {
  LIST_ELEM_CLASSNAME: [
    'w-full p-2 pl-8 text-xl font-light border-slate-300 border-2',
    'group-focus-within:border-slate-400',
  ],
  SVG_BASE_CLASSNAME: `stroke-slate-500 fill-slate-500`,
  SVG_FOCUSABLE_CLASSNAME: `stroke-slate-500 fill-slate-500 group-focus-within:stroke-slate-800 group-focus-within:fill-slate-800`,
  INPUT_CLASSNAME: `text-slate-500 focus:text-slate-800 placeholder-slate-500 group-focus-within:placeholder-slate-800 placeholder:font-extralight`,
} as const;

// 2. Simplify logging
const log = {
  info: (...args: any[]) => CONFIG.LOG && console.log(...args),
  error: (...args: any[]) => CONFIG.LOG && console.error(...args),
};

interface PropTypes {
  isServerRendered?: boolean;
}

interface User {
  id: number;
  name: string;
  thumb?: string;
  show_path: string;
  num_posts?: number;
  distance: number;
  matched_name: string;
  domain_icon: string;
}

interface ServerResponse {
  users: User[];
}

export default function UserSearchBar({ isServerRendered }: PropTypes) {
  isServerRendered = !!isServerRendered;
  const [pendingRequest, setPendingRequest] = useState<AbortController | null>(
    null,
  );
  const [state, setState] = useState({
    userName: '',
    userList: [] as ServerResponse['users'],
    selectedIdx: null as number | null,
    errorMessage: null as string | null,
    typingSettled: true,
    isFocused: isServerRendered ? false : true,
  });

  const inputRef = useRef(null);

  const clearResults = useCallback(() => {
    setState((s) => ({
      ...s,
      userList: [],
      errorMessage: null,
      selectedIdx: null,
    }));
  }, []);

  const cancelPendingRequest = useCallback(async () => {
    if (pendingRequest) {
      setPendingRequest(null);
      pendingRequest.abort();
    }
  }, [pendingRequest, setPendingRequest]);

  const sendSearchRequest = useCallback(
    (userName) => {
      cancelPendingRequest();

      const controller = new AbortController();
      setPendingRequest(controller);

      async function sendRequest() {
        try {
          let req = await fetch(
            `${CONFIG.HOST}/users/search_by_name.json?name=${userName}`,
            {
              signal: controller.signal,
            },
          );

          setPendingRequest(null);
          setState((s) => ({
            ...s,
            errorMessage: null,
          }));

          if (req.status != 200) {
            const error_json = await req.json();
            setState((s) => ({
              ...s,
              errorMessage: `error loading users: ${error_json.error || JSON.stringify(error_json)}`,
            }));
          } else {
            let gotUserList = await req.json();
            setState((s) => ({
              ...s,
              userList: gotUserList.users,
            }));
          }
        } catch (err) {
          if (!err.message.includes('aborted')) {
            setState((s) => ({
              ...s,
              errorMessage: `error loading users: ` + err.message,
            }));
          }
        }
      }
      sendRequest();

      return () => controller.abort();
    },
    [cancelPendingRequest, setPendingRequest],
  );

  const searchForUser = useCallback(
    (userName: string) => {
      setState((s) => ({ ...s, userName }));
      if (isEmpty(userName)) {
        clearResults();
      } else {
        sendSearchRequest(userName);
      }
    },
    [clearResults, sendSearchRequest],
  );

  const searchForUserDebounced = useCallback(
    debounce(async (userName) => {
      setState((s) => ({ ...s, typingSettled: true }));
      searchForUser(userName);
    }, 250),
    [searchForUser],
  );

  function invokeIdx(idx) {
    const user = state.userList[idx];
    if (user) {
      setState((s) => ({ ...s, userName: user.name }));
      inputRef.current.value = user.name;
      window.location.href = user.show_path;
    }
  }

  function invokeSelected() {
    if (state.selectedIdx != null) {
      invokeIdx(state.selectedIdx);
    }
  }

  const visibility = {
    error: state.isFocused && !isEmpty(state.errorMessage),
    info:
      state.isFocused &&
      !isEmpty(state.userName) &&
      !pendingRequest &&
      state.typingSettled &&
      state.userList.length === 0,
    items: !isEmpty(state.userName) && state.userList.length > 0,
  };
  const anyShown = Object.values(visibility).some(Boolean);

  function UserSearchBarItems() {
    return (
      <div className="divide-y divide-slate-300 overflow-hidden border border-slate-300 bg-slate-50 shadow-lg sm:rounded-xl">
        {visibility.error ? (
          <ListItem
            key="error"
            isLast={!visibility.info && state.userList.length == 0}
            selected={false}
            style="error"
            value={state.errorMessage}
          />
        ) : null}
        {visibility.info ? (
          <ListItem
            key="info"
            isLast={!visibility.items}
            selected={false}
            style="info"
            value="No users found"
          />
        ) : null}
        {visibility.items
          ? state.userList.map(
              ({ name, thumb, show_path, num_posts, domain_icon }, idx) => (
                <ListItem
                  key={'name-' + name}
                  isLast={idx == state.userList.length - 1}
                  selected={idx == state.selectedIdx}
                  style="item"
                  value={name}
                  thumb={thumb}
                  href={show_path}
                  subtext={num_posts ? `${num_posts.toString()} posts` : ''}
                  domainIcon={domain_icon}
                />
              ),
            )
          : null}
      </div>
    );
  }

  const keyHandlers = {
    Tab: (shiftKey: boolean) =>
      shiftKey ? selectPrevListElem() : selectNextListElem(),
    ArrowDown: () => selectNextListElem(),
    ArrowUp: () => selectPrevListElem(),
    Enter: () => invokeSelected(),
  };

  function onSearchInputKeyDown(event: React.KeyboardEvent) {
    const handler = keyHandlers[event.code];
    if (handler) {
      event.preventDefault();
      handler(event.shiftKey);
    }
  }

  function selectNextListElem() {
    setNewIdxTruncated(state.selectedIdx == null ? 0 : state.selectedIdx + 1);
  }

  function selectPrevListElem() {
    setNewIdxTruncated(state.selectedIdx == null ? -1 : state.selectedIdx - 1);
  }

  function setNewIdxTruncated(newIdx) {
    if (state.userList.length == 0) {
      newIdx = null;
    } else {
      if (newIdx >= state.userList.length) {
        newIdx = 0;
      } else if (newIdx < 0) {
        newIdx = state.userList.length - 1;
      }
    }
    setState((s) => ({ ...s, selectedIdx: newIdx }));
  }

  return (
    <div
      className={[
        'relative mx-auto w-full p-2 transition-colors duration-1000 sm:rounded-xl',
        'focus-within:border-slate-400 sm:max-w-md',
        'border-slate-300 bg-slate-50 p-2 shadow-lg',
      ].join(' ')}
    >
      <label className={`relative block ${STYLES.INPUT_CLASSNAME}`}>
        <Icon
          type="magnifying-glass"
          className={`ml-2 ${STYLES.SVG_FOCUSABLE_CLASSNAME}`}
        />
        {pendingRequest && (
          <Icon
            type="spinner"
            className={`right-2 ${STYLES.SVG_BASE_CLASSNAME}`}
          />
        )}
        <input
          autoFocus
          className={[
            STYLES.LIST_ELEM_CLASSNAME,
            STYLES.INPUT_CLASSNAME,
            'rounded-lg outline-none',
            'bg-slate-50 placeholder:italic',
          ]
            .filter(Boolean)
            .join(' ')}
          placeholder="Search Users"
          defaultValue={state.userName}
          onChange={(e) => {
            setState((s) => ({ ...s, typingSettled: false }));
            searchForUserDebounced(e.target.value);
          }}
          onKeyDown={onSearchInputKeyDown}
          onFocus={() => setState((s) => ({ ...s, isFocused: true }))}
          onBlur={() => setState((s) => ({ ...s, isFocused: false }))}
          ref={inputRef}
        />
      </label>
      {anyShown && (
        <div className="absolute left-0 right-0 top-full z-50 mt-1">
          <UserSearchBarItems />
        </div>
      )}
    </div>
  );
}
