import { type FC, useEffect, useMemo, useState } from 'react';

import type { OrganismMetadata } from './OrganismMetadataTableSelector.tsx';
import useClientFlag from '../../hooks/isClient.ts';
import { routes } from '../../routes/routes.ts';
import type { InputField, InputFieldOption, Metadata } from '../../types/config.ts';
import { getUrl } from '../../utils/getUrl.ts';
import { BoxWithTabsBox, BoxWithTabsTab, BoxWithTabsTabBar } from '../common/BoxWithTabs.tsx';
import { Button } from '../common/Button.tsx';
import { FormattedText } from '../common/FormattedText.tsx';
import IwwaArrowDown from '~icons/iwwa/arrow-down';

export enum TableQueryParams {
    ORGANISM = 'organism',
    FIELD_TYPE = 'fieldType',
}

enum FieldType {
    INPUT = 'inputFields',
    GENERATED = 'generatedFields',
}

function getHeaderLinkId(header: string): string {
    return header.replaceAll(' ', '_');
}

function getFieldLinkId(header: string, name: string): string {
    return `${header.replaceAll(' ', '_')}-${name}`;
}

function scrollElementIntoView(id: string) {
    const element = document.getElementById(id);
    if (element) element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}

type MetadataSearchBarProps = {
    value: string;
    onChange: (value: string) => void;
    placeholder?: string;
};

const MetadataSearchBar: FC<MetadataSearchBarProps> = ({ value, onChange, placeholder }) => {
    return (
        <input
            type='search'
            placeholder={placeholder ?? 'Search fields...'}
            value={value}
            onChange={(e) => onChange(e.target.value)}
            className='w-1/3 border border-gray-300 px-3 py-2 rounded-md shadow-xs focus:outline-hidden focus:ring-1 focus:ring-primary-600 focus:border-primary-600'
        />
    );
};

export const OrganismMetadataTable: FC<{ organism: OrganismMetadata }> = ({ organism }) => {
    const [activeTab, setActiveTab] = useState<FieldType | null>(null);
    const [inputFieldSearch, setInputFieldSearch] = useState<string>('');
    const [generatedFieldSearch, setGeneratedFieldSearch] = useState<string>('');

    // Mapping of input fields to their corresponding metadata
    // Some input fields, such as extraInputFields in the config, do not have corresponding metadata
    const inputFieldsMetadata = useMemo(() => {
        const inputFieldNames = [
            ...new Set(
                Array.from(organism.groupedInputFields.values())
                    .flat()
                    .map((field) => field.name),
            ),
        ];

        const organismMetadataMap = new Map(organism.metadata.map((meta) => [meta.name, meta]));
        return new Map(inputFieldNames.map((fieldName) => [fieldName, organismMetadataMap.get(fieldName)]));
    }, [organism]);

    // Generated fields grouped by header
    const groupedGeneratedFields = useMemo(() => {
        const groupedFields = new Map<string, Metadata[]>();

        organism.metadata
            .filter((field: Metadata) => !inputFieldsMetadata.has(field.name) && !field.hideInSearchResultsTable)
            .forEach((field) => {
                const header = field.header ?? 'Other';
                if (groupedFields.has(header)) groupedFields.get(header)?.push(field);
                else groupedFields.set(header, [field]);
            });
        return groupedFields;
    }, [organism, inputFieldsMetadata]);

    // Set the initial active tab based on the URL parameter
    useEffect(() => {
        const params = new URLSearchParams(window.location.search);
        setActiveTab(
            params.get(TableQueryParams.FIELD_TYPE) === FieldType.GENERATED ? FieldType.GENERATED : FieldType.INPUT,
        );
    }, []);

    // Mapping of link IDs to their corresponding tab(s)
    const linkIdTabs = useMemo(() => {
        const map = new Map<string, FieldType[]>();

        // Add input field headers and fields
        organism.groupedInputFields.forEach((inputFields, header) => {
            const headerLinkId = getHeaderLinkId(header);
            map.set(headerLinkId, [FieldType.INPUT]);

            inputFields.forEach((field) => {
                const fieldLinkId = getFieldLinkId(header, field.name);
                map.set(fieldLinkId, [FieldType.INPUT]);
            });
        });

        // Add generated field headers and fields
        groupedGeneratedFields.forEach((generatedFields, header) => {
            const headerLinkId = getHeaderLinkId(header);
            if (map.has(headerLinkId)) map.get(headerLinkId)?.push(FieldType.GENERATED);
            else map.set(headerLinkId, [FieldType.GENERATED]);

            generatedFields.forEach((field) => {
                const fieldLinkId = getFieldLinkId(header, field.name);
                if (map.has(fieldLinkId)) map.get(fieldLinkId)?.push(FieldType.GENERATED);
                else map.set(fieldLinkId, [FieldType.GENERATED]);
            });
        });

        return map;
    }, [organism, groupedGeneratedFields]);

    // Scroll to the element in the URL hash when the component mounts or the active tab changes
    // Uses the linkIdTabs mapping to determine which tab(s) the link ID belongs to,
    // only scrolling if the active tab matches
    useEffect(() => {
        if (!activeTab) return;
        const linkId = window.location.hash.slice(1); // Remove the '#' from the hash
        if (linkIdTabs.get(linkId)?.includes(activeTab)) scrollElementIntoView(linkId);
    }, [activeTab, linkIdTabs]);

    const handleTabSelect = (fieldType: FieldType) => {
        setActiveTab(fieldType);
        const params = new URLSearchParams(window.location.search);
        params.set(TableQueryParams.FIELD_TYPE, fieldType);
        const newUrl = getUrl(window.location.origin, window.location.pathname, params);
        window.history.replaceState({ path: newUrl }, '', newUrl);
    };

    return (
        <div className='mt-6 mb-2'>
            <h1 className='text-2xl font-bold mb-4'>{organism.displayName}</h1>
            <BoxWithTabsTabBar>
                <BoxWithTabsTab
                    key={FieldType.INPUT.valueOf()}
                    isActive={activeTab === FieldType.INPUT}
                    onClick={() => handleTabSelect(FieldType.INPUT)}
                    label='Input fields'
                    className='text-base'
                />
                <BoxWithTabsTab
                    key={FieldType.GENERATED.valueOf()}
                    isActive={activeTab === FieldType.GENERATED}
                    onClick={() => handleTabSelect(FieldType.GENERATED)}
                    label='Generated fields'
                    className='text-base'
                />
            </BoxWithTabsTabBar>
            <BoxWithTabsBox>
                {activeTab === FieldType.INPUT && (
                    <div className='mt-4'>
                        <div className='mb-4'>
                            You can download a submission template with an overview of input metadata fields (with their
                            descriptions and examples) here:{' '}
                            <a
                                href={routes.metadataTemplate(organism.key, 'submit', 'xlsx')}
                                className='text-primary-700 opacity-90'
                            >
                                {`${organism.displayName.replaceAll(' ', '_')}_metadata_template.xlsx`}
                            </a>
                        </div>
                        <MetadataSearchBar
                            value={inputFieldSearch}
                            onChange={setInputFieldSearch}
                            placeholder='Search input fields...'
                        />
                        {Array.from(organism.groupedInputFields.entries()).map(([header, inputFields]) => {
                            const typedInputFields = inputFields.map((field) => ({
                                ...field,
                                type: inputFieldsMetadata.get(field.name)?.type ?? 'string',
                            }));
                            return (
                                <MetadataTableSection
                                    key={header}
                                    header={header}
                                    fields={typedInputFields}
                                    search={inputFieldSearch}
                                    isInputFields
                                />
                            );
                        })}
                    </div>
                )}
                {activeTab === FieldType.GENERATED && (
                    <div className='mt-4'>
                        <MetadataSearchBar
                            value={generatedFieldSearch}
                            onChange={setGeneratedFieldSearch}
                            placeholder='Search generated fields...'
                        />
                        {Array.from(groupedGeneratedFields.entries()).map(([header, generatedFields]) => (
                            <MetadataTableSection
                                key={header}
                                header={header}
                                fields={generatedFields}
                                search={generatedFieldSearch}
                            />
                        ))}
                    </div>
                )}
            </BoxWithTabsBox>
        </div>
    );
};

type TypedInputField = InputField & { type: string };

type MetadataTableProps =
    | {
          header: string;
          fields: Metadata[];
          search: string;
          isInputFields?: false;
      }
    | {
          header: string;
          fields: TypedInputField[];
          search: string;
          isInputFields: true;
      };

function containsSearch(header: string, field: Metadata | TypedInputField, search: string): boolean {
    const searchTokens = search.toLowerCase().trim().split(/\s+/);
    const contents = [
        header.toLowerCase(),
        field.name.toLowerCase(),
        field.type.toLowerCase(),
        field.displayName?.toLowerCase() ?? '',
        field.definition?.toLowerCase() ?? '',
        'guidance' in field ? (field.guidance?.toLowerCase() ?? '') : '',
        'example' in field ? (field.example?.toString().toLowerCase() ?? '') : '',
    ].join(' ');

    return searchTokens.every((searchToken) => contents.includes(searchToken));
}

const MetadataTableSection: FC<MetadataTableProps> = (props) => {
    const [expandedHeader, setExpandedHeader] = useState<boolean>(true);
    const filteredFields = props.search
        ? props.fields.filter((field) => containsSearch(props.header, field, props.search))
        : props.fields;
    if (filteredFields.length === 0) return <></>;

    return (
        <div className='mb-8'>
            <h3
                id={getHeaderLinkId(props.header)}
                className='text-lg font-semibold mb-4 pt-1 cursor-pointer'
                onClick={() => setExpandedHeader((prev) => !prev)}
            >
                {props.header}
                <IwwaArrowDown
                    className={`inline-block -mt-1 ml-1 h-4 w-4 transition-transform duration-300 ${
                        expandedHeader ? '' : '-rotate-90'
                    }`}
                />
            </h3>
            <div
                className={`transition-all duration-300 ${expandedHeader ? 'block' : 'sr-only'}`}
                data-table-header={props.header}
            >
                {props.isInputFields ? (
                    <MetadataTable {...props} fields={filteredFields as TypedInputField[]} />
                ) : (
                    <MetadataTable {...props} fields={filteredFields as Metadata[]} />
                )}
            </div>
        </div>
    );
};

const FieldNameCell: FC<{ header: string; field: TypedInputField | Metadata; fieldType: FieldType }> = ({
    header,
    field,
    fieldType,
}) => {
    const handleFieldLink = () => {
        const params = new URLSearchParams(window.location.search);
        params.set(TableQueryParams.FIELD_TYPE, fieldType);
        const fieldLinkId = getFieldLinkId(header, field.name);
        const newUrl = getUrl(window.location.origin, window.location.pathname, params, fieldLinkId);
        window.history.replaceState({ path: newUrl }, '', newUrl);
        scrollElementIntoView(fieldLinkId);
    };

    return (
        <div className='flex flex-col'>
            <div className='group flex items-center gap-2'>
                <span>{field.name}</span>
                <Button
                    onClick={handleFieldLink}
                    className='opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-primary-600'
                    title='Link to this field'
                >
                    ¶
                </Button>
            </div>
            <span className='text-xs text-gray-500 mt-1'>{field.displayName}</span>
        </div>
    );
};

type AllowedValuesListProps = {
    options: InputFieldOption[];
    fieldName: string;
};

export const AllowedValuesList: FC<AllowedValuesListProps> = ({ options, fieldName }) => {
    const [query, setQuery] = useState('');
    const [copied, setCopied] = useState(false);
    const isClient = useClientFlag();

    const trimmedQuery = query.trim();

    const filtered = useMemo(
        () =>
            trimmedQuery === ''
                ? options
                : options.filter((o) => o.name.toLowerCase().includes(trimmedQuery.toLowerCase())),
        [options, trimmedQuery],
    );

    const handleCopy = async () => {
        await navigator.clipboard.writeText(filtered.map((o) => o.name).join('\n'));
        setCopied(true);
        setTimeout(() => setCopied(false), 2000);
    };

    return (
        <div className='flex flex-col gap-1'>
            <label htmlFor={`allowed-values-search-${fieldName}`} className='text-sm font-medium text-primary-600'>
                Search available options
            </label>
            <input
                id={`allowed-values-search-${fieldName}`}
                type='text'
                placeholder={`Search ${options.length} values…`}
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                disabled={!isClient}
                className='border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-primary-500 disabled:bg-gray-100'
            />
            <ul
                className='max-h-40 overflow-y-auto border border-gray-200 rounded text-sm'
                aria-label={`Allowed values for ${fieldName}`}
            >
                {filtered.length === 0 ? (
                    <li className='px-2 py-1 text-gray-400 italic'>No matches</li>
                ) : (
                    filtered.map((o) => (
                        <li key={o.name} className='px-2 py-1 even:bg-gray-50'>
                            {o.name}
                        </li>
                    ))
                )}
            </ul>
            <div className='flex items-center gap-2'>
                {trimmedQuery !== '' && (
                    <span className='text-xs text-gray-400'>
                        {filtered.length} of {options.length}
                    </span>
                )}
                <Button
                    onClick={() => void handleCopy()}
                    disabled={!isClient}
                    title={`Copy ${filtered.length} value${filtered.length === 1 ? '' : 's'} to clipboard`}
                    className='text-xs px-2 py-1 border border-gray-300 rounded text-gray-600 hover:text-primary-600 hover:border-primary-500 whitespace-nowrap'
                >
                    {copied ? 'Copied!' : 'Copy values'}
                </Button>
            </div>
        </div>
    );
};

const MetadataTable: FC<MetadataTableProps> = (props) => {
    return (
        <table className='table-auto border-collapse border border-gray-200 w-full'>
            <thead>
                <tr>
                    <th className='border border-gray-300 px-4 py-2 w-[25%]'>Field name</th>
                    {props.isInputFields && <th className='border border-gray-300 px-4 py-2 w-[13%]'>Type</th>}
                    <th className='border border-gray-300 px-4 py-2 w-[37%]'>Description</th>
                    {props.isInputFields && <th className='border border-gray-300 px-4 py-2 w-[25%]'>Example</th>}
                </tr>
            </thead>
            <tbody>
                {props.isInputFields
                    ? props.fields.map((field) => {
                          return (
                              <tr id={getFieldLinkId(props.header, field.name)} key={field.name}>
                                  <td className='border border-gray-300 px-4 py-2'>
                                      <FieldNameCell header={props.header} field={field} fieldType={FieldType.INPUT} />
                                  </td>
                                  <td className='border border-gray-300 px-4 py-2'>
                                      {field.options && field.options.length > 0 ? <span>enum</span> : field.type}
                                  </td>
                                  <td className='border border-gray-300 px-4 py-2'>
                                      <FormattedText
                                          text={[field.definition, field.guidance].filter(Boolean).join(' ')}
                                          formatLinks
                                      />
                                      {field.options && field.options.length > 0 && (
                                          <div className='mt-2'>
                                              <AllowedValuesList options={field.options} fieldName={field.name} />
                                          </div>
                                      )}
                                  </td>
                                  <td className='border border-gray-300 px-4 py-2'>{field.example ?? ''}</td>
                              </tr>
                          );
                      })
                    : props.fields.map((field) => (
                          <tr id={getFieldLinkId(props.header, field.name)} key={field.name}>
                              <td className='border border-gray-300 px-4 py-2'>
                                  <FieldNameCell header={props.header} field={field} fieldType={FieldType.GENERATED} />
                              </td>
                              <td className='border border-gray-300 px-4 py-2'>
                                  <FormattedText text={field.definition ?? ''} formatLinks />
                              </td>
                          </tr>
                      ))}
            </tbody>
        </table>
    );
};
