import {Fragment, useRef} from 'react';
import styled from '@emotion/styled';
import {Observer} from 'mobx-react';

import Alert from 'sentry/components/alert';
import AlertLink from 'sentry/components/alertLink';
import NumberField from 'sentry/components/forms/fields/numberField';
import SelectField from 'sentry/components/forms/fields/selectField';
import SentryMemberTeamSelectorField from 'sentry/components/forms/fields/sentryMemberTeamSelectorField';
import SentryProjectSelectorField from 'sentry/components/forms/fields/sentryProjectSelectorField';
import TextField from 'sentry/components/forms/fields/textField';
import Form, {FormProps} from 'sentry/components/forms/form';
import FormModel from 'sentry/components/forms/model';
import ExternalLink from 'sentry/components/links/externalLink';
import List from 'sentry/components/list';
import ListItem from 'sentry/components/list/listItem';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
import Text from 'sentry/components/text';
import {timezoneOptions} from 'sentry/data/timezones';
import {t, tct, tn} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {SelectValue} from 'sentry/types';
import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
import slugify from 'sentry/utils/slugify';
import commonTheme from 'sentry/utils/theme';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import useProjects from 'sentry/utils/useProjects';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import {crontabAsText} from 'sentry/views/monitors/utils';

import {
  IntervalConfig,
  Monitor,
  MonitorConfig,
  MonitorType,
  ScheduleType,
} from '../types';

const SCHEDULE_OPTIONS: SelectValue<string>[] = [
  {value: ScheduleType.CRONTAB, label: t('Crontab')},
  {value: ScheduleType.INTERVAL, label: t('Interval')},
];

const DEFAULT_MONITOR_TYPE = 'cron_job';
const DEFAULT_CRONTAB = '0 0 * * *';

// Maps the value from the SentryMemberTeamSelectorField -> the expected alert
// rule key and vice-versa.
//
// XXX(epurkhiser): For whatever reason the rules API wants the team and member
// to be capitalized.
const RULE_TARGET_MAP = {team: 'Team', member: 'Member'} as const;
const RULES_SELECTOR_MAP = {Team: 'team', Member: 'member'} as const;

// In minutes
export const DEFAULT_MAX_RUNTIME = 30;
export const DEFAULT_CHECKIN_MARGIN = 1;
const CHECKIN_MARGIN_MINIMUM = 1;
const TIMEOUT_MINIMUM = 1;

const getIntervals = (n: number): SelectValue<string>[] => [
  {value: 'minute', label: tn('minute', 'minutes', n)},
  {value: 'hour', label: tn('hour', 'hours', n)},
  {value: 'day', label: tn('day', 'days', n)},
  {value: 'week', label: tn('week', 'weeks', n)},
  {value: 'month', label: tn('month', 'months', n)},
  {value: 'year', label: tn('year', 'years', n)},
];

type Props = {
  apiEndpoint: string;
  apiMethod: FormProps['apiMethod'];
  onSubmitSuccess: FormProps['onSubmitSuccess'];
  monitor?: Monitor;
  submitLabel?: string;
};

interface TransformedData extends Partial<Omit<Monitor, 'config' | 'alertRule'>> {
  alertRule?: Partial<Monitor['alertRule']>;
  config?: Partial<Monitor['config']>;
}

/**
 * Transform sub-fields for what the API expects
 */
function transformData(_data: Record<string, any>, model: FormModel) {
  const result = model.fields.toJSON().reduce<TransformedData>((data, [k, v]) => {
    data.config ??= {};
    data.alertRule ??= {};

    if (k === 'alertRule.targets') {
      const alertTargets = (v as string[] | undefined)?.map(item => {
        // See SentryMemberTeamSelectorField to understand why these are strings
        const [type, id] = item.split(':');

        const targetType = RULE_TARGET_MAP[type];

        return {targetType, targetIdentifier: Number(id)};
      });

      data.alertRule.targets = alertTargets;
      return data;
    }

    if (k === 'alertRule.environment') {
      const environment = v === '' ? undefined : (v as string);
      data.alertRule.environment = environment;
      return data;
    }

    if (k === 'config.schedule.frequency' || k === 'config.schedule.interval') {
      if (!Array.isArray(data.config.schedule)) {
        data.config.schedule = [1, 'hour'];
      }
    }

    if (Array.isArray(data.config.schedule) && k === 'config.schedule.frequency') {
      data.config.schedule[0] = parseInt(v as string, 10);
      return data;
    }

    if (Array.isArray(data.config.schedule) && k === 'config.schedule.interval') {
      data.config.schedule[1] = v as IntervalConfig['schedule'][1];
      return data;
    }

    if (k.startsWith('config.')) {
      data.config[k.substring(7)] = v;
      return data;
    }

    data[k] = v;
    return data;
  }, {});

  // If targets are not specified, don't send alert rule config to backend
  if (!result.alertRule?.targets) {
    result.alertRule = undefined;
  }

  return result;
}

/**
 * Transform config field errors from the error response
 */
function mapFormErrors(responseJson?: any) {
  if (responseJson.config === undefined) {
    return responseJson;
  }

  // Bring nested config entries to the top
  const {config, ...responseRest} = responseJson;
  const configErrors = Object.fromEntries(
    Object.entries(config).map(([key, value]) => [`config.${key}`, value])
  );

  return {...responseRest, ...configErrors};
}

function MonitorForm({
  monitor,
  submitLabel,
  apiEndpoint,
  apiMethod,
  onSubmitSuccess,
}: Props) {
  const form = useRef(new FormModel({transformData, mapFormErrors}));
  const organization = useOrganization();
  const {projects} = useProjects();
  const {selection} = usePageFilters();

  function formDataFromConfig(type: MonitorType, config: MonitorConfig) {
    const rv = {};
    switch (type) {
      case 'cron_job':
        rv['config.schedule_type'] = config.schedule_type;
        rv['config.checkin_margin'] = config.checkin_margin;
        rv['config.max_runtime'] = config.max_runtime;
        rv['config.failure_issue_threshold'] = config.failure_issue_threshold;
        rv['config.recovery_threshold'] = config.recovery_threshold;

        switch (config.schedule_type) {
          case 'interval':
            rv['config.schedule.frequency'] = config.schedule[0];
            rv['config.schedule.interval'] = config.schedule[1];
            break;
          case 'crontab':
          default:
            rv['config.schedule'] = config.schedule;
            rv['config.timezone'] = config.timezone;
        }
        break;
      default:
    }
    return rv;
  }

  const selectedProjectId = selection.projects[0];
  const selectedProject = selectedProjectId
    ? projects.find(p => p.id === selectedProjectId + '')
    : null;

  const isSuperuser = isActiveSuperuser();
  const filteredProjects = projects.filter(project => isSuperuser || project.isMember);

  const alertRuleTarget = monitor?.alertRule?.targets.map(
    target => `${RULES_SELECTOR_MAP[target.targetType]}:${target.targetIdentifier}`
  );

  const envOptions = selectedProject?.environments.map(e => ({value: e, label: e})) ?? [];
  const alertRuleEnvs = [
    {
      label: 'All Environments',
      value: '',
    },
    ...envOptions,
  ];

  const hasIssuePlatform = organization.features.includes('issue-platform');

  return (
    <Form
      allowUndo
      requireChanges
      apiEndpoint={apiEndpoint}
      apiMethod={apiMethod}
      model={form.current}
      initialData={
        monitor
          ? {
              name: monitor.name,
              slug: monitor.slug,
              type: monitor.type ?? DEFAULT_MONITOR_TYPE,
              project: monitor.project.slug,
              'alertRule.targets': alertRuleTarget,
              'alertRule.environment': monitor.alertRule?.environment,
              ...formDataFromConfig(monitor.type, monitor.config),
            }
          : {
              project: selectedProject ? selectedProject.slug : null,
              type: DEFAULT_MONITOR_TYPE,
            }
      }
      onSubmitSuccess={onSubmitSuccess}
      submitLabel={submitLabel}
    >
      <StyledList symbol="colored-numeric">
        <StyledListItem>{t('Add a name and project')}</StyledListItem>
        <ListItemSubText>{t('The name will show up in notifications.')}</ListItemSubText>
        <InputGroup>
          <StyledTextField
            name="name"
            placeholder={t('My Cron Job')}
            required
            stacked
            inline={false}
          />
          {monitor && (
            <StyledTextField
              name="slug"
              help={tct(
                'The [strong:monitor-slug] is used to uniquely identify your monitor within your organization. Changing this slug will require updates to any instrumented check-in calls.',
                {strong: <strong />}
              )}
              placeholder={t('monitor-slug')}
              required
              stacked
              inline={false}
              transformInput={slugify}
            />
          )}
          <StyledSentryProjectSelectorField
            name="project"
            projects={filteredProjects}
            placeholder={t('Choose Project')}
            disabled={!!monitor}
            disabledReason={t('Existing monitors cannot be moved between projects')}
            valueIsSlug
            required
            stacked
            inline={false}
          />
        </InputGroup>
        <StyledListItem>{t('Set your schedule')}</StyledListItem>
        <ListItemSubText>
          {tct('You can use [link:the crontab syntax] or our interval schedule.', {
            link: <ExternalLink href="https://en.wikipedia.org/wiki/Cron" />,
          })}
        </ListItemSubText>
        <InputGroup>
          {monitor !== undefined && (
            <StyledAlert type="info">
              {t(
                'Any changes you make to the execution schedule will only be applied after the next expected check-in.'
              )}
            </StyledAlert>
          )}
          <StyledSelectField
            name="config.schedule_type"
            options={SCHEDULE_OPTIONS}
            defaultValue={ScheduleType.CRONTAB}
            orientInline
            required
            stacked
            inline={false}
          />
          <Observer>
            {() => {
              const scheduleType = form.current.getValue('config.schedule_type');

              const parsedSchedule =
                scheduleType === 'crontab'
                  ? crontabAsText(
                      form.current.getValue('config.schedule')?.toString() ?? ''
                    )
                  : null;

              if (scheduleType === 'crontab') {
                return (
                  <MultiColumnInput columns="1fr 2fr">
                    <StyledTextField
                      name="config.schedule"
                      placeholder="* * * * *"
                      defaultValue={DEFAULT_CRONTAB}
                      css={{input: {fontFamily: commonTheme.text.familyMono}}}
                      required
                      stacked
                      inline={false}
                    />
                    <StyledSelectField
                      name="config.timezone"
                      defaultValue="UTC"
                      options={timezoneOptions}
                      required
                      stacked
                      inline={false}
                    />
                    {parsedSchedule && <CronstrueText>"{parsedSchedule}"</CronstrueText>}
                  </MultiColumnInput>
                );
              }
              if (scheduleType === 'interval') {
                return (
                  <MultiColumnInput columns="auto 1fr 2fr">
                    <LabelText>{t('Every')}</LabelText>
                    <StyledNumberField
                      name="config.schedule.frequency"
                      placeholder="e.g. 1"
                      defaultValue="1"
                      required
                      stacked
                      inline={false}
                    />
                    <StyledSelectField
                      name="config.schedule.interval"
                      options={getIntervals(
                        Number(form.current.getValue('config.schedule.frequency') ?? 1)
                      )}
                      defaultValue="day"
                      required
                      stacked
                      inline={false}
                    />
                  </MultiColumnInput>
                );
              }
              return null;
            }}
          </Observer>
        </InputGroup>
        <StyledListItem>{t('Set thresholds')}</StyledListItem>
        <ListItemSubText>
          {t('Configure when we mark your monitor as failed or missed.')}
        </ListItemSubText>
        <InputGroup>
          <Panel>
            <PanelBody>
              <NumberField
                name="config.checkin_margin"
                min={CHECKIN_MARGIN_MINIMUM}
                placeholder={tn(
                  'Defaults to %s minute',
                  'Defaults to %s minutes',
                  DEFAULT_CHECKIN_MARGIN
                )}
                help={t('Number of minutes before a check-in is considered missed.')}
                label={t('Grace Period')}
              />
              <NumberField
                name="config.max_runtime"
                min={TIMEOUT_MINIMUM}
                placeholder={tn(
                  'Defaults to %s minute',
                  'Defaults to %s minutes',
                  DEFAULT_MAX_RUNTIME
                )}
                help={t(
                  'Number of a minutes before an in-progress check-in is marked timed out.'
                )}
                label={t('Max Runtime')}
              />
            </PanelBody>
          </Panel>
        </InputGroup>
        <StyledListItem>{t('Notifications')}</StyledListItem>
        <ListItemSubText>{t('Configure who to notify and when.')}</ListItemSubText>
        <InputGroup>
          <Panel>
            <PanelBody>
              {monitor?.config.alert_rule_id && (
                <AlertLink
                  priority="muted"
                  to={normalizeUrl(
                    `/alerts/rules/${monitor.project.slug}/${monitor.config.alert_rule_id}/`
                  )}
                  withoutMarginBottom
                >
                  {t('Customize this monitors notification configuration in Alerts')}
                </AlertLink>
              )}
              <SentryMemberTeamSelectorField
                label={t('Notify')}
                help={t('Send notifications to a member or team.')}
                name="alertRule.targets"
                multiple
                menuPlacement="auto"
              />
              {hasIssuePlatform && (
                <Fragment>
                  <NumberField
                    name="config.failure_issue_threshold"
                    min={1}
                    placeholder="1"
                    help={t('Create an issue after this many missed or error check-ins')}
                    label={t('Tolerate Failures')}
                  />
                  <NumberField
                    name="config.recovery_threshold"
                    min={1}
                    placeholder="1"
                    help={t('Recover monitor status after this many healthy check-ins')}
                    label={t('Recovery Threshold')}
                  />
                </Fragment>
              )}
              <SelectField
                label={t('Environment')}
                help={t('Only receive notifications from a specific environment.')}
                name="alertRule.environment"
                options={alertRuleEnvs}
                menuPlacement="auto"
                defaultValue=""
              />
            </PanelBody>
          </Panel>
        </InputGroup>
      </StyledList>
    </Form>
  );
}

export default MonitorForm;

const StyledList = styled(List)`
  width: 800px;
`;

const StyledAlert = styled(Alert)`
  margin-bottom: 0;
`;

const StyledNumberField = styled(NumberField)`
  padding: 0;
`;

const StyledSelectField = styled(SelectField)`
  padding: 0;
`;

const StyledTextField = styled(TextField)`
  padding: 0;
`;

const StyledSentryProjectSelectorField = styled(SentryProjectSelectorField)`
  padding: 0;
`;

const StyledListItem = styled(ListItem)`
  font-size: ${p => p.theme.fontSizeExtraLarge};
  font-weight: bold;
  line-height: 1.3;
`;

const LabelText = styled(Text)`
  font-weight: bold;
  color: ${p => p.theme.subText};
`;

const ListItemSubText = styled(Text)`
  padding-left: ${space(4)};
  color: ${p => p.theme.subText};
`;

const InputGroup = styled('div')`
  padding-left: ${space(4)};
  margin-top: ${space(1)};
  margin-bottom: ${space(4)};
  display: flex;
  flex-direction: column;
  gap: ${space(1)};
`;

const MultiColumnInput = styled('div')<{columns?: string}>`
  display: grid;
  align-items: center;
  gap: ${space(1)};
  grid-template-columns: ${p => p.columns};
`;

const CronstrueText = styled(LabelText)`
  font-weight: normal;
  font-size: ${p => p.theme.fontSizeExtraSmall};
  font-family: ${p => p.theme.text.familyMono};
  grid-column: auto / span 2;
`;
