import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router';

import { Box, Button, CircularProgress, Link, Typography } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import CloseIcon from '@mui/icons-material/Close';
import InfoIcon from '@mui/icons-material/Info';
import { makeStyles } from 'tss-react/mui';

import { darkBackground, gray, red } from '../../../themes/common';
import { notify } from '../../common/notify';
import { apiErrorMessage } from '@/utils/api';
import { CollapsibleSection } from '../components/CollapsibleSection';
import { DivergedDialog } from '../components/DivergedDialog';
import { MasterfilesVersionDialog } from '../components/MasterfilesVersionDialog';
import { ModuleInputData } from '../components/ModuleInputData';
import { SanitizedHtml } from '../../common/SanitizedHtml';
import { UpdateModuleDialog } from '../components/UpdateModuleDialog';
import { MASTER_VERSION, MASTERFILES_MODULE, MASTERFILES_REPOSITORY } from '../constants';
import { useAppDispatch, useAppSelector } from '../store';
import { fetchModule } from '../store/modulesSlice/thunks';
import { selectCurrentModule } from '../store/modulesSlice/selectors';
import {
  addModuleToProject,
  forcePullProject,
  forceRebaseProject,
  loadProjectInfo,
  pushProject,
  rebaseProject,
  refreshProjectStatus,
  removeModuleFromProject,
  updateModuleVersion
} from '../store/projectsSlice/thunks';
import { selectAddedModules, selectCurrentProject } from '../store/projectsSlice/selectors';
import { removeProjectAhead, setProjectAhead } from '../utils/projectAhead';
import { formatModuleDate } from '../utils/format';
import { decodeHtml } from '../utils/text';

const useStyles = makeStyles()(theme => ({
  // Counteract the Layout main's left padding so the module content area is
  // flush with the sidebar's right border; section dividers then run edge-to-
  // edge to the sidebar. Inner text is indented via SECTION_INSET.
  main: { marginLeft: -30 },
  title: {
    marginTop: 40,
    fontWeight: 700,
    fontSize: 32,
    lineHeight: '36px',
    color: theme.palette.text.primary,
    position: 'relative',
    '&::before': {
      content: '""',
      background:
        theme.palette.mode === 'dark'
          ? 'url(/images/build/build_icon_big_dark.svg) no-repeat'
          : 'url(/images/build/build_icon_big.svg) no-repeat',
      width: 41,
      height: 41,
      left: -50,
      top: 0,
      position: 'absolute'
    }
  },
  titleExternal: { '&::before': { filter: 'grayscale(100%)' } },
  top: {
    borderBottom: `1px solid ${theme.palette.divider}`,
    paddingBottom: 12,
    paddingLeft: 75,
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'flex-end'
  },
  actionButton: { whiteSpace: 'nowrap', flexShrink: 0 },
  notOnBuild: { marginTop: 40, paddingLeft: 50, fontStyle: 'italic', fontWeight: 500, fontSize: 16, color: gray[600] },
  detailRow: { marginTop: 12, fontWeight: 500, fontSize: 16, '& > span:first-of-type': { display: 'inline-block', width: 107 } },
  updateLink: {
    display: 'inline-flex',
    alignItems: 'center',
    gap: 4,
    marginLeft: 20,
    padding: '2px 10px',
    borderRadius: 4,
    border: `1px solid ${theme.palette.mode === 'dark' ? red[300] : red[600]}`,
    textDecoration: 'none',
    color: theme.palette.mode === 'dark' ? red[300] : red[600],
    '&:hover': { textDecoration: 'none' }
  },
  description: { fontWeight: 500, fontSize: 16, lineHeight: '24px' },
  readme: {
    marginTop: 12,
    fontWeight: 500,
    fontSize: 16,
    lineHeight: '24px',
    '& h1, & h2, & h3, & h4': { marginTop: '1.6rem', marginBottom: '1.6rem' },
    '& h1': { paddingBottom: '1.6rem' },
    '& h2': { paddingBottom: '0.6rem' },
    '& p': { margin: '1.6rem 0', maxWidth: 999 },
    '& code': {
      background: theme.palette.mode === 'dark' ? darkBackground[50] : '#F7F7F7',
      padding: 4,
      borderRadius: 2,
      border: 'none'
    },
    '& pre code': { padding: 0, borderRadius: 0, background: 'none' },
    '& ul': {
      listStyle: 'none',
      margin: '0.6em 0',
      '& li': { marginLeft: '2rem', maxWidth: 999, listStyleImage: 'url("/images/build/orange-list-icon.svg")' }
    },
    '& ol': { listStylePosition: 'inside' }
  }
}));

export const ModulePage = ({ external = false }: { external?: boolean }) => {
  const { classes, cx } = useStyles();
  const dispatch = useAppDispatch();
  const navigate = useNavigate();
  const params = useParams();

  const rawName = params.name ?? '';
  // External module names arrive base64-encoded in the URL, so a hand-edited link
  // can hand us something `atob` refuses to decode.
  const moduleName = useMemo(() => {
    if (!external) return rawName;
    try {
      return atob(rawName);
    } catch {
      return '';
    }
  }, [external, rawName]);

  const project = useAppSelector(selectCurrentProject);
  const addedModules = useAppSelector(selectAddedModules);
  const fetchedModule = useAppSelector(selectCurrentModule);

  const [masterfilesOpen, setMasterfilesOpen] = useState(false);
  const [updateOpen, setUpdateOpen] = useState(false);
  const [busy, setBusy] = useState(false);
  const [divergedOpen, setDivergedOpen] = useState(false);

  const addedEntry = addedModules.find(m => m.name === moduleName);
  const addedVersion = addedEntry?.version;
  const projectId = project?.id ?? localStorage.getItem('projectID') ?? undefined;

  useEffect(() => {
    if (!project) {
      const stored = localStorage.getItem('projectID');
      if (stored) dispatch(loadProjectInfo(stored));
    }
  }, [project, dispatch]);

  // Internal modules are fetched from the index; external ones come straight
  // from the project's added-modules list.
  useEffect(() => {
    if (!external && moduleName) dispatch(fetchModule({ name: moduleName, version: addedVersion }));
  }, [external, moduleName, addedVersion, dispatch]);

  const module = external ? { name: moduleName, ...(addedEntry ?? {}), external: true } : fetchedModule;

  const latestVersion = useMemo(() => {
    if (external || !fetchedModule?.versions) return undefined;
    return Object.keys(fetchedModule.versions).find(v => fetchedModule.versions[v].latest);
  }, [external, fetchedModule]);

  const refreshAfterMutation = async () => {
    if (projectId == null) return;
    try {
      const status = await dispatch(refreshProjectStatus(projectId)).unwrap();
      if (status === 'ahead') setProjectAhead(projectId);
      else if (status === 'ok') removeProjectAhead(projectId);
      else if (status === 'behind') await dispatch(rebaseProject(projectId)).unwrap();
      else if (status === 'diverged') setDivergedOpen(true);
    } catch {
      /* status refresh best-effort */
    }
    dispatch(loadProjectInfo(projectId));
  };

  const onForcePush = async () => {
    if (projectId == null) return;
    setDivergedOpen(false);
    try {
      await dispatch(forceRebaseProject(projectId)).unwrap();
      await dispatch(pushProject(projectId)).unwrap();
      notify.success('Pushed local changes in a new commit.');
    } catch (err: unknown) {
      notify.error(apiErrorMessage(err, 'Something went wrong.'));
    }
    dispatch(loadProjectInfo(projectId));
  };

  const onDiscardLocal = async () => {
    if (projectId == null) return;
    setDivergedOpen(false);
    try {
      await dispatch(forcePullProject(projectId)).unwrap();
      notify.success('Local changes discarded.');
    } catch (err: unknown) {
      notify.error(apiErrorMessage(err, 'Something went wrong.'));
    }
    dispatch(loadProjectInfo(projectId));
  };

  const doAdd = (payload: { name?: string; url?: string; version?: string }) => {
    if (projectId == null) return;
    setBusy(true);
    dispatch(addModuleToProject({ projectId, ...payload }))
      .unwrap()
      .then(() => {
        notify.success('Module successfully added.');
        return refreshAfterMutation();
      })
      .catch(err => notify.error(apiErrorMessage(err, 'Unable to add module.')))
      .finally(() => setBusy(false));
  };

  const onAdd = () => {
    if (moduleName === MASTERFILES_MODULE) {
      setMasterfilesOpen(true);
    } else {
      doAdd({ name: moduleName, version: fetchedModule?.version });
    }
  };

  const onMasterfilesConfirm = (version: string) => {
    setMasterfilesOpen(false);
    doAdd(version === MASTER_VERSION ? { url: MASTERFILES_REPOSITORY } : { name: MASTERFILES_MODULE, version });
  };

  const onRemove = () => {
    if (projectId == null) return;
    const name = external ? (addedEntry?.encodedName ?? rawName) : moduleName;
    setBusy(true);
    dispatch(removeModuleFromProject({ projectId, name, encodedName: external }))
      .unwrap()
      .then(() => {
        notify.success('Module successfully removed.');
        if (external) navigate(`/project/${projectId}`);
        else return refreshAfterMutation();
      })
      .catch(err => notify.error(apiErrorMessage(err, 'Unable to remove module.')))
      .finally(() => setBusy(false));
  };

  const onUpdate = (gitMessage: string) => {
    if (projectId == null || !latestVersion) return;
    dispatch(updateModuleVersion({ projectId, name: moduleName, version: latestVersion, gitMessage }))
      .unwrap()
      .then(() => {
        notify.success('Module successfully updated.');
        setUpdateOpen(false);
        refreshAfterMutation();
      })
      .catch(err => {
        notify.error(apiErrorMessage(err, 'Unable to update module.'));
        setUpdateOpen(false);
      });
  };

  if (!module) return null;

  const isAdded = Boolean(addedVersion) || external;
  const classic = project?.classic_policy_set === true;
  const repo = 'repo' in module ? module.repo : undefined;
  const commit = 'commit' in module ? module.commit : undefined;
  const subdirectory = 'subdirectory' in module ? module.subdirectory : undefined;
  const downloads = 'downloads' in module ? module.downloads : undefined;
  const updated = 'updated' in module ? module.updated : undefined;
  const version = addedVersion ?? ('version' in module ? module.version : undefined);
  const readme = 'readme' in module ? module.readme : undefined;

  return (
    <Box className={cx(classes.main, external && 'external')}>
      <Box className={classes.top}>
        <Box>
          <Typography component="h1" className={cx(classes.title, external && classes.titleExternal)}>
            {module.name}
          </Typography>
          {'description' in module && module.description && (
            <Typography className={classes.description}>{decodeHtml(module.description)}</Typography>
          )}
        </Box>
        <Box>
          {!isAdded ? (
            <Button
              variant="outlined"
              color="inherit"
              size="small"
              className={classes.actionButton}
              startIcon={busy ? <CircularProgress size={16} color="inherit" /> : <AddIcon />}
              disabled={busy || classic || !module.name || projectId == null}
              onClick={onAdd}
            >
              Add module
            </Button>
          ) : (
            <Button
              variant="outlined"
              color="inherit"
              size="small"
              className={classes.actionButton}
              startIcon={busy ? <CircularProgress size={16} color="inherit" /> : <CloseIcon />}
              disabled={busy || projectId == null}
              onClick={onRemove}
            >
              Remove module
            </Button>
          )}
        </Box>
      </Box>

      {external && (
        <Typography className={classes.notOnBuild}>
          The module is not from the canonical index{' '}
          <Link href="https://build.cfengine.com" target="_blank" rel="noopener">
            build.cfengine.com
          </Link>
          .
        </Typography>
      )}

      <CollapsibleSection title="Details">
        {downloads != null && (
          <Box className={classes.detailRow}>
            <span>Downloads</span> {downloads}
          </Box>
        )}
        {updated && (
          <Box className={classes.detailRow}>
            <span>Updated</span> {formatModuleDate(updated)}
          </Box>
        )}
        {version && (
          <Box className={classes.detailRow}>
            <span>Version</span> {version}
            {addedVersion && latestVersion && addedVersion !== latestVersion && (
              <Link component="button" type="button" className={classes.updateLink} onClick={() => setUpdateOpen(true)}>
                <InfoIcon fontSize="inherit" /> Update available
              </Link>
            )}
          </Box>
        )}
        {repo && (
          <Box className={classes.detailRow}>
            <Link href={`${repo}/tree/${commit}/${subdirectory}`} target="_blank" rel="noopener" sx={{ mr: 2 }}>
              Repository
            </Link>
            {!external && (
              <Link href={`${repo}/issues`} target="_blank" rel="noopener">
                Report issue
              </Link>
            )}
          </Box>
        )}
      </CollapsibleSection>

      {project && !classic && projectId != null && (
        <ModuleInputData
          projectId={projectId}
          moduleName={moduleName}
          moduleAdded={isAdded}
          encodedName={external}
          onSaved={refreshAfterMutation}
        />
      )}

      {!external && readme && (
        <CollapsibleSection title="Description">
          <SanitizedHtml className={classes.readme} html={decodeHtml(readme)} />
        </CollapsibleSection>
      )}

      <MasterfilesVersionDialog
        open={masterfilesOpen}
        onClose={() => setMasterfilesOpen(false)}
        onConfirm={onMasterfilesConfirm}
      />
      <UpdateModuleDialog
        open={updateOpen}
        moduleName={moduleName}
        addedVersion={addedVersion}
        latestVersion={latestVersion}
        onClose={() => setUpdateOpen(false)}
        onConfirm={onUpdate}
      />
      <DivergedDialog
        open={divergedOpen}
        onClose={() => setDivergedOpen(false)}
        onPushInNewCommit={onForcePush}
        onDiscardLocal={onDiscardLocal}
      />
    </Box>
  );
};
