import { useEffect, useRef, useState } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router';

import {
  Alert,
  Box,
  Button,
  Checkbox,
  CircularProgress,
  FormControlLabel,
  FormLabel,
  Link,
  Radio,
  RadioGroup,
  TextField,
  Typography
} from '@mui/material';
import { makeStyles } from 'tss-react/mui';
import validator from 'validator';

import { blue, darkBlue } from '../../../themes/common';

import { notify } from '../../common/notify';
import { CopySshKeyDialog } from '../components/CopySshKeyDialog';
import { DeleteProjectDialog } from '../components/DeleteProjectDialog';
import { MasterfilesVersionDialog } from '../components/MasterfilesVersionDialog';
import { PROJECT_CREATED_KEY, REPOSITORY_URL_PATTERN } from '../constants';
import * as api from '../store/api';
import { useAppDispatch } from '../store';
import {
  createLocalProject,
  createProject,
  deleteProject,
  fetchProject,
  generateSshKey,
  updateProject,
  deleteSshKey as deleteSshKeyThunk,
  fetchSshKey
} from '../store/projectsSlice/thunks';
import type { AuthenticationType, Project, ProjectFormValues } from '../types';

const HTTP_URL = /^(https?):/i;
const SSH_URL = /^(?:git|ssh|git@(.*)):/i;

const useStyles = makeStyles()(theme => ({
  root: { maxWidth: 1160, padding: theme.spacing(3) },
  top: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
  title: { color: theme.palette.mode === 'dark' ? theme.palette.text.primary : darkBlue[900] },
  hr: { border: 0, borderTop: `1px solid ${theme.palette.divider}`, margin: '20px 0' },
  fieldRow: { display: 'flex', gap: theme.spacing(2), flexWrap: 'wrap' },
  field: { width: 560, maxWidth: '100%' },
  fieldLabel: {
    display: 'block',
    marginBottom: theme.spacing(1),
    color: theme.palette.text.primary
  },
  authLabel: { color: theme.palette.text.secondary },
  keyField: { '& .MuiInputBase-root': { backgroundColor: 'transparent' }, '& textarea': { backgroundColor: 'transparent' } },
  // `&&` beats the global `html.dark body * { color }` that otherwise whitens the required marker.
  required: { fontWeight: 500, '&&': { color: theme.palette.error.main } },
  radio: {
    // Dark theme's primary is gray; the selected radio should be blue like the original.
    ...(theme.palette.mode === 'dark' && { '&&.Mui-checked, &&.Mui-checked *': { color: blue[500] } })
  },
  buttons: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: theme.spacing(3) },
  actions: { display: 'flex', gap: theme.spacing(1) }
}));

const FieldLabel = ({
  children,
  htmlFor,
  required
}: {
  children: React.ReactNode;
  htmlFor?: string;
  required?: boolean;
}) => {
  const { classes } = useStyles();
  return (
    <Typography component="label" variant="labelSmall" htmlFor={htmlFor} className={classes.fieldLabel}>
      {children}
      {required && <span className={classes.required}> *</span>}
    </Typography>
  );
};

const emptyValues: ProjectFormValues = {
  name: '',
  branch: '',
  username: '',
  repositoryUrl: '',
  authenticationType: 'password',
  password: '',
  sshPrivateKey: '',
  sshKeyId: null,
  classicPolicySet: false
};

const toFormValues = (project: Project): ProjectFormValues => ({
  name: project.name ?? '',
  branch: project.branch ?? '',
  username: project.username ?? '',
  repositoryUrl: project.repository_url ?? '',
  authenticationType: project.authentication_type ?? 'password',
  password: '',
  sshPrivateKey: '',
  sshKeyId: project.ssh_key_id ?? null,
  classicPolicySet: project.classic_policy_set ?? false
});

export const ProjectForm = () => {
  const { classes } = useStyles();
  const dispatch = useAppDispatch();
  const navigate = useNavigate();
  const { id } = useParams();
  const isEdit = Boolean(id);

  const { control, handleSubmit, setValue, getValues, reset, trigger, formState } = useForm<ProjectFormValues>({
    defaultValues: emptyValues,
    mode: 'onChange'
  });

  const [publicKey, setPublicKey] = useState<string | null>(null);
  const [passwordPlaceholder, setPasswordPlaceholder] = useState('Password/token');
  const [sshKeyPlaceholder, setSshKeyPlaceholder] = useState('Private key');
  const [serverErrors, setServerErrors] = useState<string[]>([]);
  const [copyKeyOpen, setCopyKeyOpen] = useState(false);
  const [deleteOpen, setDeleteOpen] = useState(false);
  const [deleteSyncedWithVcs, setDeleteSyncedWithVcs] = useState(false);
  const [masterfilesOpen, setMasterfilesOpen] = useState(false);
  const [generatingKey, setGeneratingKey] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [loaded, setLoaded] = useState<Project | null>(null);

  const authType = useWatch({ control, name: 'authenticationType' });
  const repositoryUrl = useWatch({ control, name: 'repositoryUrl' });
  const branch = useWatch({ control, name: 'branch' });
  const username = useWatch({ control, name: 'username' });

  // True once the user edits the name field; stops name auto-generation.
  const nameTouched = useRef(false);
  const skipNextAutofill = useRef(false);

  useEffect(() => {
    if (!id) return;
    dispatch(fetchProject(id))
      .unwrap()
      .then(project => {
        skipNextAutofill.current = true;
        nameTouched.current = false;
        reset(toFormValues(project));
        setLoaded(project);
        setPasswordPlaceholder(`Password is ${project.password}`);
        setSshKeyPlaceholder(`Ssh private key is ${project.ssh_private_key}`);
        if (project.ssh_key_id) {
          dispatch(fetchSshKey(project.ssh_key_id))
            .unwrap()
            .then(key => setPublicKey(key.public_key))
            .catch(() => undefined);
        }
      })
      .catch(() => notify.error('Project not found.'));
  }, [id, dispatch, reset]);

  // Auto-fill the project name from `<username>/<repo>@<branch>` while the user
  // has not manually edited it and both repository URL and branch are set.
  useEffect(() => {
    if (skipNextAutofill.current) {
      skipNextAutofill.current = false;
      return;
    }
    if (nameTouched.current || !repositoryUrl || !branch) return;
    const repositoryName = repositoryUrl.substring(repositoryUrl.lastIndexOf('/') + 1).replace(/\.git$/, '');
    const generated = `${username ? `${username}/` : ''}${repositoryName}@${branch}`;
    setValue('name', generated, { shouldValidate: true });
  }, [repositoryUrl, branch, username, setValue]);

  // Repository URL validity depends on the chosen auth type, so re-check it
  // whenever the auth type flips.
  useEffect(() => {
    if (getValues('repositoryUrl')) trigger('repositoryUrl');
  }, [authType, getValues, trigger]);

  // Thunks reject with the API body already (string or {msg, details}).
  const extractErrors = (payload: unknown): void => {
    const p = payload as { details?: unknown; msg?: string } | string | undefined;
    if (p && typeof p === 'object' && p.msg) {
      notify.error(p.msg);
      setServerErrors(Array.isArray(p.details) ? (p.details as string[]) : []);
    } else {
      notify.error(typeof p === 'string' ? p : 'Something went wrong.');
    }
  };

  const goToProject = (projectId: number | string) => navigate(`/project/${projectId}`);

  const onSave = handleSubmit(async values => {
    setSubmitting(true);
    setServerErrors([]);
    try {
      const project = await dispatch(createProject(values)).unwrap();
      notify.success('Project successfully created.');
      api.refreshModulesList().catch(() => undefined);
      sessionStorage.setItem(PROJECT_CREATED_KEY, String(project.id));
      goToProject(project.id);
    } catch (err) {
      extractErrors(err);
    } finally {
      setSubmitting(false);
    }
  });

  const onUpdate = handleSubmit(async values => {
    if (!id) return;
    setSubmitting(true);
    setServerErrors([]);
    try {
      const project = await dispatch(updateProject({ id, values })).unwrap();
      notify.success('Project successfully updated.');
      goToProject(project.id);
    } catch (err) {
      extractErrors(err);
    } finally {
      setSubmitting(false);
    }
  });

  const onSetupLater = (version: string) => {
    setMasterfilesOpen(false);
    setSubmitting(true);
    dispatch(createLocalProject(version))
      .unwrap()
      .then(project => {
        notify.success('Project successfully created.');
        api.refreshModulesList().catch(() => undefined);
        sessionStorage.setItem(PROJECT_CREATED_KEY, String(project.id));
        goToProject(project.id);
      })
      .catch(extractErrors)
      .finally(() => setSubmitting(false));
  };

  const onGenerateSshKey = () => {
    setGeneratingKey(true);
    dispatch(generateSshKey())
      .unwrap()
      .then(key => {
        setValue('sshKeyId', key.id);
        setPublicKey(key.public_key);
        setCopyKeyOpen(true);
      })
      .catch(err => notify.error((err as { data?: string })?.data ?? 'Failed to generate SSH key.'))
      .finally(() => setGeneratingKey(false));
  };

  const onDeleteSshKey = () => {
    const keyId = getValues('sshKeyId');
    if (!keyId) return;
    dispatch(deleteSshKeyThunk(keyId))
      .unwrap()
      .then(() => {
        setValue('sshKeyId', null);
        setPublicKey(null);
      })
      .catch(() => notify.error('Failed to delete SSH key.'));
  };

  const onFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = e => setValue('sshPrivateKey', String(e.target?.result ?? ''), { shouldValidate: true });
    reader.onerror = () => notify.error('Error reading file.');
    reader.readAsText(file, 'UTF-8');
    event.target.value = '';
  };

  const openDeleteDialog = async () => {
    if (!id) return;
    try {
      const status = await api.vcsSynchronized(id);
      setDeleteSyncedWithVcs(status.isSynchronised);
    } catch {
      setDeleteSyncedWithVcs(false);
    }
    setDeleteOpen(true);
  };

  const onConfirmDelete = (stopDeploying: boolean) => {
    if (!id) return;
    dispatch(deleteProject({ id, stopDeploying }))
      .unwrap()
      .then(() => navigate('/'))
      .catch(extractErrors);
  };

  const repoOrBranchChanged =
    isEdit && loaded && (loaded.repository_url !== repositoryUrl || (loaded.branch ?? '') !== branch);

  return (
    <Box className={classes.root}>
      <Box className={classes.top}>
        <Typography component="h1" variant="h5" className={classes.title}>
          {isEdit ? 'Edit project' : 'Add new project'}
        </Typography>
        {isEdit && (
          <Button variant="outlined" color="error" onClick={openDeleteDialog}>
            Delete project
          </Button>
        )}
      </Box>
      <Box component="hr" className={classes.hr} />
      <Typography variant="subtitle1">
        Enter your Git repository information to set up a remote repository for your project.
      </Typography>

      {serverErrors.length > 0 && (
        <Alert severity="error" onClose={() => setServerErrors([])} sx={{ my: 2 }}>
          {serverErrors.map((error, index) => (
            <div key={index}>{error}</div>
          ))}
        </Alert>
      )}

      <Typography className={classes.required} variant="body2" sx={{ mt: 2 }}>
        *required
      </Typography>

      <form>
        <Box className={classes.fieldRow} sx={{ mt: 1 }}>
          <Box className={classes.field}>
            <FieldLabel htmlFor="repositoryUrl" required>
              Repository URL
            </FieldLabel>
            <Controller
              name="repositoryUrl"
              control={control}
              rules={{
                validate: value => {
                  if (validator.isEmpty(value, { ignore_whitespace: true })) return 'Repository URL is required.';
                  if (!REPOSITORY_URL_PATTERN.test(value)) {
                    return 'Malformed repository URL. Expected a valid git URL. Supported protocols: git, ssh, http(s)';
                  }
                  const auth = getValues('authenticationType');
                  if (auth === 'private_key' && HTTP_URL.test(value)) {
                    return 'HTTP(S) URL together with SSH key authentication type cannot be used';
                  }
                  if (auth === 'password' && SSH_URL.test(value)) {
                    return 'SSH URL together with username/password authentication type cannot be used';
                  }
                  return true;
                }
              }}
              render={({ field, fieldState }) => (
                <TextField
                  {...field}
                  id="repositoryUrl"
                  fullWidth
                  variant="outlined"
                  error={!!fieldState.error}
                  helperText={fieldState.error?.message}
                />
              )}
            />
          </Box>
          <Box className={classes.field}>
            <FieldLabel htmlFor="branch" required>
              Branch
            </FieldLabel>
            <Controller
              name="branch"
              control={control}
              rules={{ validate: value => (validator.isEmpty(value, { ignore_whitespace: true }) ? 'Branch is required.' : true) }}
              render={({ field, fieldState }) => (
                <TextField
                  {...field}
                  id="branch"
                  fullWidth
                  variant="outlined"
                  error={!!fieldState.error}
                  helperText={fieldState.error?.message}
                />
              )}
            />
          </Box>
        </Box>

        <Box sx={{ mt: 4, mb: 1 }}>
          <Controller
            name="authenticationType"
            control={control}
            render={({ field }) => (
              <RadioGroup row {...field} onChange={(_, value) => field.onChange(value as AuthenticationType)}>
                <FormLabel className={classes.authLabel} sx={{ alignSelf: 'center', mr: 2 }}>
                  Authentication type:
                </FormLabel>
                <FormControlLabel
                  value="password"
                  control={<Radio className={classes.radio} />}
                  label="Username/password"
                />
                <FormControlLabel
                  value="private_key"
                  control={<Radio className={classes.radio} />}
                  label="SSH key"
                />
              </RadioGroup>
            )}
          />
        </Box>

        {authType === 'password' && (
          <Box className={classes.fieldRow} sx={{ mt: 1 }}>
            <Box className={classes.field}>
              <FieldLabel htmlFor="username" required>
                Username
              </FieldLabel>
              <Controller
                name="username"
                control={control}
                rules={{ validate: value => (validator.isEmpty(value, { ignore_whitespace: true }) ? 'Username is required.' : true) }}
                render={({ field, fieldState }) => (
                  <TextField
                    {...field}
                    id="username"
                    fullWidth
                    variant="outlined"
                    error={!!fieldState.error}
                    helperText={fieldState.error?.message}
                  />
                )}
              />
            </Box>
            <Box className={classes.field}>
              <FieldLabel htmlFor="password" required>
                Password/token
              </FieldLabel>
              <Controller
                name="password"
                control={control}
                rules={{ validate: value => (validator.isEmpty(value, { ignore_whitespace: true }) ? 'Password/token is required.' : true) }}
                render={({ field, fieldState }) => (
                  <TextField
                    {...field}
                    id="password"
                    fullWidth
                    variant="outlined"
                    type="password"
                    placeholder={passwordPlaceholder}
                    error={!!fieldState.error}
                    helperText={fieldState.error?.message}
                  />
                )}
              />
            </Box>
          </Box>
        )}

        {authType === 'private_key' &&
          (!publicKey ? (
            <Box sx={{ mt: 1 }}>
              <Box className={classes.fieldRow} sx={{ alignItems: 'center' }}>
                <Box className={classes.field}>
                  <FieldLabel htmlFor="sshPrivateKey" required>
                    Private key
                  </FieldLabel>
                  <Controller
                    name="sshPrivateKey"
                    control={control}
                    rules={{
                      validate: value =>
                        getValues('authenticationType') === 'private_key' &&
                        !getValues('sshKeyId') &&
                        validator.isEmpty(value, { ignore_whitespace: true })
                          ? 'Private key is required.'
                          : true
                    }}
                    render={({ field, fieldState }) => (
                      <TextField
                        {...field}
                        id="sshPrivateKey"
                        fullWidth
                        variant="outlined"
                        placeholder={sshKeyPlaceholder}
                        multiline
                        minRows={4}
                        error={!!fieldState.error}
                        helperText={fieldState.error?.message}
                      />
                    )}
                  />
                </Box>
                <Box sx={{ alignSelf: 'center' }}>Or</Box>
                <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
                  <Typography variant="body2">Upload a key file:</Typography>
                  <Button variant="outlined" color="inherit" component="label">
                    Choose a file
                    <input type="file" hidden onChange={onFileSelected} />
                  </Button>
                </Box>
              </Box>
              <Typography variant="body2" sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
                <Link
                  component="button"
                  type="button"
                  onClick={onGenerateSshKey}
                  disabled={generatingKey}
                  sx={{ '&:disabled': { color: 'text.disabled', cursor: 'default' } }}
                >
                  Generate
                </Link>{' '}
                a public/private key pair
                {generatingKey && <CircularProgress size={16} />}
              </Typography>
            </Box>
          ) : (
            <Box sx={{ mt: 1 }}>
              <FieldLabel required>Public key</FieldLabel>
              <Typography variant="body2" sx={{ mb: 1 }}>
                Copy this key into the setting of your git provider, for example, Github.
              </Typography>
              <TextField
                className={`${classes.field} ${classes.keyField}`}
                variant="outlined"
                value={publicKey}
                multiline
                minRows={4}
                slotProps={{ input: { readOnly: true } }}
              />
              <Typography variant="body2" sx={{ mt: 1 }}>
                <Link component="button" type="button" color="error" onClick={onDeleteSshKey}>
                  Delete
                </Link>{' '}
                generated key pair
              </Typography>
            </Box>
          ))}

        <Box component="hr" className={classes.hr} />

        <Controller
          name="classicPolicySet"
          control={control}
          render={({ field }) => (
            <FormControlLabel
              control={
                <Checkbox checked={field.value} onChange={e => field.onChange(e.target.checked)} />
              }
              label="Classic policy set (not cfbs):"
            />
          )}
        />

        {repoOrBranchChanged && (
          <Alert severity="warning" sx={{ mt: 2, maxWidth: 500 }}>
            By changing the repository url or branch you will initialize a new project and the current one will be
            removed from the file system and not pushed changes will be lost.
          </Alert>
        )}

        <Box className={classes.buttons}>
          <Box className={classes.field}>
            <FieldLabel htmlFor="name">Project name</FieldLabel>
            <Controller
              name="name"
              control={control}
              render={({ field }) => (
                <TextField
                  {...field}
                  id="name"
                  fullWidth
                  variant="outlined"
                  onChange={e => {
                    nameTouched.current = true;
                    field.onChange(e);
                  }}
                />
              )}
            />
          </Box>
          <Box className={classes.actions}>
            {!isEdit && (
              <Button variant="text" color="inherit" onClick={() => setMasterfilesOpen(true)} disabled={submitting}>
                Set up later
              </Button>
            )}
            <Button variant="text" color="inherit" onClick={() => navigate(isEdit ? `/project/${id}` : '/')}>
              Cancel
            </Button>
            <Button
              variant="contained"
              color="secondary"
              disabled={submitting || !formState.isValid}
              onClick={isEdit ? onUpdate : onSave}
            >
              {isEdit ? 'Update' : 'Save'}
            </Button>
          </Box>
        </Box>
      </form>

      <CopySshKeyDialog open={copyKeyOpen} publicKey={publicKey ?? ''} onClose={() => setCopyKeyOpen(false)} />
      <DeleteProjectDialog
        open={deleteOpen}
        projectName={loaded?.name ?? ''}
        synchronizedWithVcs={deleteSyncedWithVcs}
        onClose={() => setDeleteOpen(false)}
        onDelete={onConfirmDelete}
      />
      <MasterfilesVersionDialog
        open={masterfilesOpen}
        onClose={() => setMasterfilesOpen(false)}
        onConfirm={onSetupLater}
      />
    </Box>
  );
};
