import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { CircularProgress } from '@mui/material';
import { makeStyles } from 'tss-react/mui';

import ConfirmationDialog from '../ConfirmationDialog';
import { EditProfileDialog } from './EditProfileDialog';
import { EditSubscriptionsDialog } from './EditSubscriptionsDialog';
import { NewProfileDialog } from './NewProfileDialog';
import { NodeFormDialog, type NodeFormMode } from './NodeFormDialog';
import { ProfileDropdown } from './ProfileDropdown';
import { isTrue } from './utils';
import {
  createProfile,
  deleteProfile,
  fetchHostCount,
  fetchHostCountBatch,
  fetchProfileData,
  fetchProfiles,
  NO_CATEGORY_LABEL,
  ROOT_LABEL,
  refreshTree,
  selectCountsLoading,
  selectNodeTree,
  selectProfiles,
  selectProfilesStatus,
  selectSelectedProfile,
  selectSelectedProfileId,
  selectTreeError,
  selectTreeRefreshKey,
  selectTreeStatus,
  setSelectedProfileId,
  setUncategorized,
  updateProfileData
} from './store';
import { TreeNode } from './TreeNode';
import type { NodeContext, NodeDescription, Profile } from './types';
import { useHostTreeDispatch } from './store/dispatchType';
import {
  clearStoredProfileId,
  loadStoredProfileId,
  storeProfileId,
  UNCATEGORIZED_SENTINEL,
  useExpandedKeys
} from './useTreeState';

const useStyles = makeStyles()(theme => ({
  host: {
    display: 'flex',
    flexDirection: 'column',
    minHeight: 0,
    height: '100%'
  },
  header: {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginBottom: 12,
    '& h4': {
      margin: 0
    }
  },
  menu: {
    marginBottom: 8,
    position: 'relative'
  },
  // .tree carries id="astrolabe" so the legacy hostNavigationTree.less rules
  // drive border, row dividers, indentation, AND `overflow-y: auto`. Override
  // the legacy `max-height: 75%` + `margin: 20px 0` so the flex column drives
  // sizing.
  tree: {
    flex: 1,
    minHeight: 0,
    maxHeight: 'none !important',
    margin: '0 !important'
  },
  message: {
    padding: 12,
    color: theme.palette.text.muted
  },
  loader: {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    padding: '40px 0'
  }
}));

interface HostsTreeProps {
  selectedHostkey?: string;
  onHostSelect?: (hostkey: string, hostname: string) => void;
  onNodeSelect?: (ctx: NodeContext) => void;
  defaultProfileName?: string;
}

// Recursively serialize a subtree the way the legacy widget did:
// `{ label, classRegex, children }` only — strip helper flags and the
// synthetic "hosts without category" sibling that is added on render.
const serializeChildren = (children: NodeDescription[] | undefined): NodeDescription[] =>
  (children ?? [])
    .filter(child => child.label !== NO_CATEGORY_LABEL)
    .map(child => ({
      label: child.label,
      classRegex: child.classRegex,
      children: serializeChildren(child.children)
    }));

// Immutable tree updates keyed by label-path (root path is ['All hosts']).
const updateAtPath = (
  tree: NodeDescription,
  pathFromRoot: string[],
  updater: (node: NodeDescription) => NodeDescription
): NodeDescription => {
  if (pathFromRoot.length === 0 || pathFromRoot[0] !== tree.label) return tree;
  if (pathFromRoot.length === 1) return updater(tree);
  const [, ...rest] = pathFromRoot;
  const head = rest[0];
  return {
    ...tree,
    children: (tree.children ?? []).map(child =>
      child.label === head ? updateAtPath(child, rest, updater) : child
    )
  };
};

const removeAtPath = (tree: NodeDescription, pathFromRoot: string[]): NodeDescription => {
  if (pathFromRoot.length < 2) return tree;
  const parentPath = pathFromRoot.slice(0, -1);
  const labelToRemove = pathFromRoot[pathFromRoot.length - 1];
  return updateAtPath(tree, parentPath, parent => ({
    ...parent,
    children: (parent.children ?? []).filter(c => c.label !== labelToRemove)
  }));
};

const collectCountQueries = (
  node: NodeDescription,
  parentIncludes: string[],
  parentLabels: string[],
  depth: number,
  out: { id: string; includes: string; excludes: string; path: string[] }[]
) => {
  const includes = depth === 0 ? [] : [...parentIncludes, node.classRegex];
  const labels = [...parentLabels, node.label];
  const excludes: string[] = [];
  if (node.excludes) excludes.push(node.excludes);
  for (const child of node.children ?? []) {
    if (child.classRegex && child.label) excludes.unshift(child.classRegex);
  }
  out.push({
    id: labels.join('|'),
    includes: includes.join(','),
    excludes: excludes.join('|'),
    path: labels
  });
  for (const child of node.children ?? []) {
    collectCountQueries(child, includes, labels, depth + 1, out);
  }
};

export const HostsTree = ({
  selectedHostkey,
  onHostSelect,
  onNodeSelect,
  defaultProfileName = 'OS'
}: HostsTreeProps) => {
  const { classes } = useStyles();
  const dispatch = useHostTreeDispatch();

  const profiles = useSelector(selectProfiles);
  const profilesStatus = useSelector(selectProfilesStatus);
  const selectedProfile = useSelector(selectSelectedProfile);
  const selectedProfileId = useSelector(selectSelectedProfileId);
  const treeStatus = useSelector(selectTreeStatus);
  const treeError = useSelector(selectTreeError);
  const treeRefreshKey = useSelector(selectTreeRefreshKey);
  const nodeTree = useSelector(selectNodeTree);
  const countsLoading = useSelector(selectCountsLoading);

  const { isExpanded, toggle: toggleExpanded, setExpandedKey } = useExpandedKeys();

  // Load profile list on mount. After it arrives, pick the default profile
  // following the legacy precedence: stored cookie/local-storage > a profile
  // whose name matches `defaultProfileName` > the one flagged as default.
  useEffect(() => {
    if (profilesStatus === 'idle') dispatch(fetchProfiles());
  }, [dispatch, profilesStatus]);

  useEffect(() => {
    if (profilesStatus !== 'loaded') return;
    if (selectedProfileId !== null) return;
    const stored = loadStoredProfileId();
    // User explicitly picked Uncategorized in a prior session — honor it
    // instead of falling back to the default profile.
    if (stored === UNCATEGORIZED_SENTINEL) return;
    const fromStorage = stored ? profiles.find(p => String(p.id) === stored) : undefined;
    const profileToSelect =
      fromStorage ??
      profiles.find(p => p.name === defaultProfileName) ??
      profiles.find(p => isTrue(p.defaultTree));
    if (profileToSelect) {
      dispatch(setSelectedProfileId(profileToSelect.id));
    }
  }, [dispatch, profilesStatus, profiles, selectedProfileId, defaultProfileName]);

  // Uncategorized (id === null after profiles loaded) renders an empty tree —
  // just the synthetic "All hosts" root with no sub-categories.
  useEffect(() => {
    if (selectedProfileId === null && profilesStatus === 'loaded') dispatch(setUncategorized());
  }, [dispatch, selectedProfileId, profilesStatus]);

  const treeFetchArgs = useMemo(
    () =>
      selectedProfile
        ? {
            profileId: selectedProfile.id,
            sharedTree: isTrue(selectedProfile.shared) || isTrue(selectedProfile.sharedTree),
            sharedFrom: selectedProfile.sharedFrom
          }
        : null,
    [selectedProfile]
  );

  // Every `fetchProfiles()` hands back new profile objects, so keying the fetch
  // on a primitive stops a profile-list reload from racing a tree load for the
  // profile that was selected before it.
  const treeFetchKey = treeFetchArgs
    ? [
        treeFetchArgs.profileId,
        treeFetchArgs.sharedTree,
        treeFetchArgs.sharedFrom ?? '',
        treeRefreshKey
      ].join('|')
    : null;

  useEffect(() => {
    if (!treeFetchArgs) return;
    dispatch(fetchProfileData(treeFetchArgs));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dispatch, treeFetchKey]);

  // Compute counts. The /host/updateCount batch endpoint ignores `excludes`,
  // so the "hosts without category" node — whose count depends entirely on
  // excluding every top-level classRegex — is fetched via /host/count.
  useEffect(() => {
    if (!nodeTree) return;
    const queries: { id: string; includes: string; excludes: string; path: string[] }[] = [];
    collectCountQueries(nodeTree, [], [], 0, queries);
    if (queries.length === 0) return;

    const isNoCategory = (path: string[]) => path[path.length - 1] === NO_CATEGORY_LABEL;
    const noCategoryQuery = queries.find(q => isNoCategory(q.path));
    const batchQueries = queries
      .slice(1) // skip root, fetched separately
      .filter(q => !isNoCategory(q.path));

    dispatch(
      fetchHostCount({ includes: [], excludes: [], pathKey: queries[0].path.join('|') })
    );

    if (batchQueries.length > 0) {
      dispatch(
        fetchHostCountBatch(
          batchQueries.map(q => ({
            id: q.id,
            includes: q.includes,
            excludes: q.excludes,
            pathKey: q.path.join('|')
          }))
        )
      );
    }

    if (noCategoryQuery) {
      dispatch(
        fetchHostCount({
          includes: noCategoryQuery.includes ? noCategoryQuery.includes.split(',') : [],
          excludes: noCategoryQuery.excludes ? [noCategoryQuery.excludes] : [],
          pathKey: noCategoryQuery.path.join('|')
        })
      );
    }
  }, [dispatch, nodeTree]);

  const countKeyFor = useCallback((path: string[]) => path.join('|'), []);

  const handleSelectProfile = useCallback(
    (p: Profile | null) => {
      dispatch(setSelectedProfileId(p?.id ?? null));
      storeProfileId(p?.id ?? null);
    },
    [dispatch]
  );

  // Make sure the "All hosts" root is expanded after the tree loads.
  useEffect(() => {
    if (!nodeTree) return;
    setExpandedKey(ROOT_LABEL, true);
  }, [nodeTree, setExpandedKey]);

  // ---- Node CRUD (dialog state stays local — pure UI state) ----
  const [nodeForm, setNodeForm] = useState<
    | null
    | {
        mode: NodeFormMode;
        targetPath: string[];
        initialLabel: string;
        initialClassRegex: string;
      }
  >(null);
  const [removeTarget, setRemoveTarget] = useState<string[] | null>(null);

  const persistTree = useCallback(
    async (nextTree: NodeDescription) => {
      if (!selectedProfile) return;
      const serialized = serializeChildren(nextTree.children);
      await dispatch(updateProfileData({ profileId: selectedProfile.id, nodeData: serialized }));
      // Re-pull the canonical tree from the server so we don't fight the
      // synthetic super-node wrapper rebuild.
      dispatch(refreshTree());
    },
    [dispatch, selectedProfile]
  );

  const applyTreeChange = useCallback(
    async (mutate: (tree: NodeDescription) => NodeDescription) => {
      if (!nodeTree) return;
      const next = mutate(nodeTree);
      await persistTree(next);
    },
    [nodeTree, persistTree]
  );

  const handleRequestAdd = useCallback((path: string[]) => {
    setNodeForm({ mode: 'Add', targetPath: path, initialLabel: '', initialClassRegex: '' });
  }, []);

  const handleRequestEdit = useCallback((path: string[], node: NodeDescription) => {
    setNodeForm({
      mode: 'Update',
      targetPath: path,
      initialLabel: node.label,
      initialClassRegex: node.classRegex ?? ''
    });
  }, []);

  const handleRequestRemove = useCallback((path: string[]) => {
    setRemoveTarget(path);
  }, []);

  const handleSubmitNodeForm = useCallback(
    async ({ label, classRegex }: { label: string; classRegex: string }) => {
      if (!nodeForm) return;
      const { mode, targetPath } = nodeForm;
      if (mode === 'Add') {
        await applyTreeChange(tree =>
          updateAtPath(tree, targetPath, parent => ({
            ...parent,
            children: [...(parent.children ?? []), { label, classRegex, children: [] }]
          }))
        );
        setExpandedKey(targetPath.join('|'), true);
      } else {
        await applyTreeChange(tree =>
          updateAtPath(tree, targetPath, node => ({ ...node, label, classRegex }))
        );
      }
      setNodeForm(null);
    },
    [nodeForm, applyTreeChange, setExpandedKey]
  );

  const handleConfirmRemove = useCallback(async () => {
    if (!removeTarget) return;
    const path = removeTarget;
    setRemoveTarget(null);
    await applyTreeChange(tree => removeAtPath(tree, path));
  }, [removeTarget, applyTreeChange]);

  // ---- Profile CRUD ----
  const [newProfileOpen, setNewProfileOpen] = useState(false);
  const [editProfileTarget, setEditProfileTarget] = useState<Profile | null>(null);
  const [editSubsOpen, setEditSubsOpen] = useState(false);
  const [deleteProfileTarget, setDeleteProfileTarget] = useState<Profile | null>(null);

  const handleCreateProfile = useCallback(
    async (payload: {
      name: string;
      subcategories: { label: string; classRegex: string }[];
      sharedPermission: string[];
      global: boolean;
    }) => {
      const action = await dispatch(
        createProfile({
          name: payload.name,
          nodeDescription: payload.subcategories,
          sharedPermission: payload.sharedPermission,
          global: payload.global
        })
      );
      if (createProfile.fulfilled.match(action)) {
        const newId = action.payload.id;
        // Refresh profile list and auto-select the newly-created one.
        const listAction = await dispatch(fetchProfiles());
        if (fetchProfiles.fulfilled.match(listAction)) {
          const created = listAction.payload.find(p => String(p.id) === String(newId));
          if (created) {
            dispatch(setSelectedProfileId(created.id));
            storeProfileId(created.id);
          }
        }
      }
      setNewProfileOpen(false);
    },
    [dispatch]
  );

  const handleDeleteProfile = useCallback(async () => {
    if (!deleteProfileTarget) return;
    const target = deleteProfileTarget;
    setDeleteProfileTarget(null);
    const action = await dispatch(deleteProfile(target.id));
    if (deleteProfile.fulfilled.match(action)) {
      if (String(selectedProfileId ?? '') === String(target.id)) clearStoredProfileId();
    }
  }, [dispatch, deleteProfileTarget, selectedProfileId]);

  const subscribedIds = useMemo(
    () => new Set(profiles.map(p => String(p.id))),
    [profiles]
  );

  const isLoading =
    profilesStatus === 'loading' || treeStatus === 'loading' || countsLoading;

  const treeContent = useMemo(() => {
    if (isLoading && !nodeTree) {
      return (
        <div className={classes.loader}>
          <CircularProgress size={28} />
        </div>
      );
    }
    if (treeError) return <div className="alert alert-error">{treeError}</div>;
    if (!nodeTree) return <div className={classes.message}>No tree to display</div>;
    return (
      <ul className="rootContainer">
        <TreeNode
          node={nodeTree}
          parentIncludes={[]}
          parentLabels={[]}
          siblingExcludes={[]}
          countKeyFor={countKeyFor}
          isExpanded={isExpanded}
          toggleExpanded={toggleExpanded}
          selectedHostkey={selectedHostkey}
          onHostSelect={onHostSelect}
          onNodeSelect={onNodeSelect}
          onRequestAdd={handleRequestAdd}
          onRequestEdit={handleRequestEdit}
          onRequestRemove={handleRequestRemove}
          depth={0}
        />
      </ul>
    );
  }, [
    isLoading,
    treeError,
    nodeTree,
    countKeyFor,
    isExpanded,
    toggleExpanded,
    selectedHostkey,
    onHostSelect,
    onNodeSelect,
    handleRequestAdd,
    handleRequestEdit,
    handleRequestRemove,
    classes.loader,
    classes.message
  ]);

  return (
    <div className={classes.host}>
      <div className={classes.header}>
        <h4 className="bold">Hosts</h4>
        <a
          className="refresh"
          href="#"
          onClick={e => {
            e.preventDefault();
            if (!isLoading) dispatch(refreshTree());
          }}
        >
          <i className="bi bi-arrow-clockwise" /> Refresh
        </a>
      </div>
      <div className={classes.menu}>
        <ProfileDropdown
          profiles={profiles}
          selectedId={selectedProfileId}
          disabled={isLoading}
          onSelect={handleSelectProfile}
          onAddNew={() => setNewProfileOpen(true)}
          onEditSubscriptions={() => setEditSubsOpen(true)}
          onEditProfile={p => setEditProfileTarget(p)}
          onDeleteProfile={p => setDeleteProfileTarget(p)}
        />
      </div>
      <div id="astrolabe" className={classes.tree}>
        <div className="listContainer">{treeContent}</div>
      </div>

      <NodeFormDialog
        open={nodeForm !== null}
        mode={nodeForm?.mode ?? 'Add'}
        initialLabel={nodeForm?.initialLabel}
        initialClassRegex={nodeForm?.initialClassRegex}
        onClose={() => setNodeForm(null)}
        onSubmit={handleSubmitNodeForm}
      />

      <ConfirmationDialog
        open={removeTarget !== null}
        onClose={() => setRemoveTarget(null)}
        onConfirm={handleConfirmRemove}
        title="Remove this node?"
        message="Are you sure you want to permanently remove this tree node?"
        confirmText="Remove"
        confirmColor="error"
        cancelText="Cancel"
      />

      <NewProfileDialog
        open={newProfileOpen}
        existingNames={profiles.map(p => p.name)}
        onClose={() => setNewProfileOpen(false)}
        onSubmit={handleCreateProfile}
      />

      <EditProfileDialog
        open={editProfileTarget !== null}
        profileId={editProfileTarget?.id ?? null}
        existingNames={profiles
          .filter(p => editProfileTarget && String(p.id) !== String(editProfileTarget.id))
          .map(p => p.name)}
        onClose={() => setEditProfileTarget(null)}
        onSaved={() => {
          setEditProfileTarget(null);
          dispatch(fetchProfiles());
        }}
      />

      <EditSubscriptionsDialog
        open={editSubsOpen}
        subscribedIds={subscribedIds}
        onClose={() => setEditSubsOpen(false)}
        onChanged={() => dispatch(fetchProfiles())}
      />

      <ConfirmationDialog
        open={deleteProfileTarget !== null}
        onClose={() => setDeleteProfileTarget(null)}
        onConfirm={handleDeleteProfile}
        title="Delete categorization?"
        message={
          deleteProfileTarget
            ? `Are you sure you want to delete "${deleteProfileTarget.name}"? This action cannot be undone.`
            : ''
        }
        confirmText="Delete"
        confirmColor="error"
        cancelText="Cancel"
      />
    </div>
  );
};
