import {memo, useEffect, useState} from 'react';
import styled from '@emotion/styled';
import * as Sentry from '@sentry/react';
import {Location} from 'history';

import {addErrorMessage} from 'app/actionCreators/indicator';
import {Client} from 'app/api';
import ErrorBoundary from 'app/components/errorBoundary';
import EventContexts from 'app/components/events/contexts';
import EventContextSummary from 'app/components/events/contextSummary/contextSummary';
import EventDevice from 'app/components/events/device';
import EventErrors, {Error} from 'app/components/events/errors';
import EventAttachments from 'app/components/events/eventAttachments';
import EventCause from 'app/components/events/eventCause';
import EventCauseEmpty from 'app/components/events/eventCauseEmpty';
import EventDataSection from 'app/components/events/eventDataSection';
import EventExtraData from 'app/components/events/eventExtraData/eventExtraData';
import EventSdk from 'app/components/events/eventSdk';
import EventTags from 'app/components/events/eventTags/eventTags';
import EventGroupingInfo from 'app/components/events/groupingInfo';
import EventPackageData from 'app/components/events/packageData';
import RRWebIntegration from 'app/components/events/rrwebIntegration';
import EventSdkUpdates from 'app/components/events/sdkUpdates';
import {DataSection} from 'app/components/events/styles';
import EventUserFeedback from 'app/components/events/userFeedback';
import ExternalLink from 'app/components/links/externalLink';
import {t, tct} from 'app/locale';
import space from 'app/styles/space';
import {
  ExceptionValue,
  Group,
  IssueAttachment,
  Organization,
  Project,
  SharedViewOrganization,
} from 'app/types';
import {DebugFile} from 'app/types/debugFiles';
import {Image} from 'app/types/debugImage';
import {Entry, EntryType, Event} from 'app/types/event';
import {Thread} from 'app/types/events';
import {isNotSharedOrganization} from 'app/types/utils';
import {defined, objectIsEmpty} from 'app/utils';
import {analytics} from 'app/utils/analytics';
import withApi from 'app/utils/withApi';
import withOrganization from 'app/utils/withOrganization';
import {projectProcessingIssuesMessages} from 'app/views/settings/project/projectProcessingIssues';

import findBestThread from './interfaces/threads/threadSelector/findBestThread';
import getThreadException from './interfaces/threads/threadSelector/getThreadException';
import EventEntry from './eventEntry';
import EventTagsAndScreenshot from './eventTagsAndScreenshot';

const MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH =
  /^(([\w\$]\.[\w\$]{1,2})|([\w\$]{2}\.[\w\$]\.[\w\$]))(\.|$)/g;

type ProGuardErrors = Array<Error>;

type Props = {
  /**
   * The organization can be the shared view on a public issue view.
   */
  organization: Organization | SharedViewOrganization;
  project: Project;
  location: Location;
  api: Client;
  event?: Event;
  group?: Group;
  isShare?: boolean;
  showExampleCommit?: boolean;
  showTagSummary?: boolean;
  isBorderless?: boolean;
  className?: string;
};

const EventEntries = memo(
  ({
    organization,
    project,
    location,
    api,
    event,
    group,
    className,
    isShare = false,
    showExampleCommit = false,
    showTagSummary = true,
    isBorderless = false,
  }: Props) => {
    const [isLoading, setIsLoading] = useState(true);
    const [proGuardErrors, setProGuardErrors] = useState<ProGuardErrors>([]);
    const [attachments, setAttachments] = useState<IssueAttachment[]>([]);

    const orgSlug = organization.slug;
    const projectSlug = project.slug;
    const orgFeatures = organization?.features ?? [];

    const hasEventAttachmentsFeature = orgFeatures.includes('event-attachments');

    useEffect(() => {
      checkProGuardError();
      recordIssueError();
      fetchAttachments();
    }, []);

    function recordIssueError() {
      if (!event || !event.errors || !(event.errors.length > 0)) {
        return;
      }

      const errors = event.errors;
      const errorTypes = errors.map(errorEntries => errorEntries.type);
      const errorMessages = errors.map(errorEntries => errorEntries.message);

      const orgId = organization.id;
      const platform = project.platform;

      analytics('issue_error_banner.viewed', {
        org_id: orgId ? parseInt(orgId, 10) : null,
        group: event?.groupID,
        error_type: errorTypes,
        error_message: errorMessages,
        ...(platform && {platform}),
      });
    }

    async function fetchProguardMappingFiles(query: string): Promise<Array<DebugFile>> {
      try {
        const proguardMappingFiles = await api.requestPromise(
          `/projects/${orgSlug}/${projectSlug}/files/dsyms/`,
          {
            method: 'GET',
            query: {
              query,
              file_formats: 'proguard',
            },
          }
        );
        return proguardMappingFiles;
      } catch (error) {
        Sentry.captureException(error);
        // do nothing, the UI will not display extra error details
        return [];
      }
    }

    function isDataMinified(str: string | null) {
      if (!str) {
        return false;
      }

      return !![...str.matchAll(MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH)].length;
    }

    function hasThreadOrExceptionMinifiedFrameData(
      definedEvent: Event,
      bestThread?: Thread
    ) {
      if (!bestThread) {
        const exceptionValues: Array<ExceptionValue> =
          definedEvent.entries?.find(e => e.type === EntryType.EXCEPTION)?.data?.values ??
          [];

        return !!exceptionValues.find(exceptionValue =>
          exceptionValue.stacktrace?.frames?.find(frame => isDataMinified(frame.module))
        );
      }

      const threadExceptionValues = getThreadException(definedEvent, bestThread)?.values;

      return !!(threadExceptionValues
        ? threadExceptionValues.find(threadExceptionValue =>
            threadExceptionValue.stacktrace?.frames?.find(frame =>
              isDataMinified(frame.module)
            )
          )
        : bestThread?.stacktrace?.frames?.find(frame => isDataMinified(frame.module)));
    }

    async function checkProGuardError() {
      if (!event || event.platform !== 'java') {
        setIsLoading(false);
        return;
      }

      const hasEventErrorsProGuardMissingMapping = event.errors?.find(
        error => error.type === 'proguard_missing_mapping'
      );

      if (hasEventErrorsProGuardMissingMapping) {
        setIsLoading(false);
        return;
      }

      const newProGuardErrors: ProGuardErrors = [];

      const debugImages = event.entries?.find(e => e.type === EntryType.DEBUGMETA)?.data
        .images as undefined | Array<Image>;

      // When debugImages contains a 'proguard' entry, it must always be only one entry
      const proGuardImage = debugImages?.find(
        debugImage => debugImage?.type === 'proguard'
      );

      const proGuardImageUuid = proGuardImage?.uuid;

      // If an entry is of type 'proguard' and has 'uuid',
      // it means that the Sentry Gradle plugin has been executed,
      // otherwise the proguard id wouldn't be in the event.
      // But maybe it failed to upload the mappings file
      if (defined(proGuardImageUuid)) {
        if (isShare) {
          setIsLoading(false);
          return;
        }

        const proguardMappingFiles = await fetchProguardMappingFiles(proGuardImageUuid);

        if (!proguardMappingFiles.length) {
          newProGuardErrors.push({
            type: 'proguard_missing_mapping',
            message: projectProcessingIssuesMessages.proguard_missing_mapping,
            data: {mapping_uuid: proGuardImageUuid},
          });
        }

        setProGuardErrors(newProGuardErrors);
        setIsLoading(false);
        return;
      } else {
        if (proGuardImage) {
          Sentry.withScope(function (s) {
            s.setLevel(Sentry.Severity.Warning);
            if (event.sdk) {
              s.setTag('offending.event.sdk.name', event.sdk.name);
              s.setTag('offending.event.sdk.version', event.sdk.version);
            }
            Sentry.captureMessage('Event contains proguard image but not uuid');
          });
        }
      }

      const threads: Array<Thread> =
        event.entries?.find(e => e.type === EntryType.THREADS)?.data?.values ?? [];

      const bestThread = findBestThread(threads);
      const hasThreadOrExceptionMinifiedData = hasThreadOrExceptionMinifiedFrameData(
        event,
        bestThread
      );

      if (hasThreadOrExceptionMinifiedData) {
        newProGuardErrors.push({
          type: 'proguard_potentially_misconfigured_plugin',
          message: tct(
            'Some frames appear to be minified. Did you configure the [plugin]?',
            {
              plugin: (
                <ExternalLink href="https://docs.sentry.io/platforms/android/proguard/#gradle">
                  Sentry Gradle Plugin
                </ExternalLink>
              ),
            }
          ),
        });

        // This capture will be removed once we're confident with the level of effectiveness
        Sentry.withScope(function (s) {
          s.setLevel(Sentry.Severity.Warning);
          if (event.sdk) {
            s.setTag('offending.event.sdk.name', event.sdk.name);
            s.setTag('offending.event.sdk.version', event.sdk.version);
          }
          Sentry.captureMessage(
            !proGuardImage
              ? 'No Proguard is used at all, but a frame did match the regex'
              : "Displaying ProGuard warning 'proguard_potentially_misconfigured_plugin' for suspected event"
          );
        });
      }

      setProGuardErrors(newProGuardErrors);
      setIsLoading(false);
    }

    async function fetchAttachments() {
      if (!event || isShare || !hasEventAttachmentsFeature) {
        return;
      }

      try {
        const response = await api.requestPromise(
          `/projects/${orgSlug}/${projectSlug}/events/${event.id}/attachments/`
        );
        setAttachments(response);
      } catch (error) {
        Sentry.captureException(error);
        addErrorMessage('An error occurred while fetching attachments');
      }
    }

    function renderEntries(definedEvent: Event) {
      const entries = definedEvent.entries;

      if (!Array.isArray(entries)) {
        return null;
      }

      return (entries as Array<Entry>).map((entry, entryIdx) => (
        <ErrorBoundary
          key={`entry-${entryIdx}`}
          customComponent={
            <EventDataSection type={entry.type} title={entry.type}>
              <p>{t('There was an error rendering this data.')}</p>
            </EventDataSection>
          }
        >
          <EventEntry
            projectSlug={projectSlug}
            group={group}
            organization={organization}
            event={definedEvent}
            entry={entry}
          />
        </ErrorBoundary>
      ));
    }

    async function handleDeleteAttachment(attachmentId: IssueAttachment['id']) {
      if (!event) {
        return;
      }

      try {
        await api.requestPromise(
          `/projects/${orgSlug}/${projectSlug}/events/${event.id}/attachments/${attachmentId}/`,
          {
            method: 'DELETE',
          }
        );

        setAttachments(attachments.filter(attachment => attachment.id !== attachmentId));
      } catch (error) {
        Sentry.captureException(error);
        addErrorMessage('An error occurred while deleteting the attachment');
      }
    }

    if (!event) {
      return (
        <LatestEventNotAvailable>
          <h3>{t('Latest Event Not Available')}</h3>
        </LatestEventNotAvailable>
      );
    }

    const hasMobileScreenshotsFeature = orgFeatures.includes('mobile-screenshots');
    const hasContext = !objectIsEmpty(event.user) || !objectIsEmpty(event.contexts);
    const hasErrors = !objectIsEmpty(event.errors) || !!proGuardErrors.length;

    return (
      <div className={className} data-test-id={`event-entries-loading-${isLoading}`}>
        {hasErrors && !isLoading && (
          <EventErrors
            event={event}
            orgSlug={orgSlug}
            projectSlug={projectSlug}
            proGuardErrors={proGuardErrors}
          />
        )}
        {!isShare &&
          isNotSharedOrganization(organization) &&
          (showExampleCommit ? (
            <EventCauseEmpty
              event={event}
              organization={organization}
              project={project}
            />
          ) : (
            <EventCause
              organization={organization}
              project={project}
              event={event}
              group={group}
            />
          ))}
        {event.userReport && group && (
          <StyledEventUserFeedback
            report={event.userReport}
            orgId={orgSlug}
            issueId={group.id}
            includeBorder={!hasErrors}
          />
        )}
        {showTagSummary &&
          (hasMobileScreenshotsFeature ? (
            <EventTagsAndScreenshot
              event={event}
              organization={organization as Organization}
              projectId={projectSlug}
              location={location}
              isShare={isShare}
              hasContext={hasContext}
              isBorderless={isBorderless}
              attachments={attachments}
              onDeleteScreenshot={handleDeleteAttachment}
            />
          ) : (
            (!!(event.tags ?? []).length || hasContext) && (
              <StyledEventDataSection title={t('Tags')} type="tags">
                {hasContext && <EventContextSummary event={event} />}
                <EventTags
                  event={event}
                  organization={organization as Organization}
                  projectId={projectSlug}
                  location={location}
                />
              </StyledEventDataSection>
            )
          ))}
        {renderEntries(event)}
        {hasContext && <EventContexts group={group} event={event} />}
        {event && !objectIsEmpty(event.context) && <EventExtraData event={event} />}
        {event && !objectIsEmpty(event.packages) && <EventPackageData event={event} />}
        {event && !objectIsEmpty(event.device) && <EventDevice event={event} />}
        {!isShare && hasEventAttachmentsFeature && (
          <EventAttachments
            event={event}
            orgId={orgSlug}
            projectId={projectSlug}
            location={location}
            attachments={attachments}
            onDeleteAttachment={handleDeleteAttachment}
          />
        )}
        {event.sdk && !objectIsEmpty(event.sdk) && <EventSdk sdk={event.sdk} />}
        {!isShare && event?.sdkUpdates && event.sdkUpdates.length > 0 && (
          <EventSdkUpdates event={{sdkUpdates: event.sdkUpdates, ...event}} />
        )}
        {!isShare && event.groupID && (
          <EventGroupingInfo
            projectId={projectSlug}
            event={event}
            showGroupingConfig={orgFeatures.includes('set-grouping-config')}
          />
        )}
        {!isShare && hasEventAttachmentsFeature && (
          <RRWebIntegration event={event} orgId={orgSlug} projectId={projectSlug} />
        )}
      </div>
    );
  }
);

const LatestEventNotAvailable = styled('div')`
  padding: ${space(2)} ${space(4)};
`;

const ErrorContainer = styled('div')`
  /*
  Remove border on adjacent context summary box.
  Once that component uses emotion this will be harder.
  */
  & + .context-summary {
    border-top: none;
  }
`;

const BorderlessEventEntries = styled(EventEntries)`
  & ${/* sc-selector */ DataSection} {
    padding: ${space(3)} 0 0 0;
  }
  & ${/* sc-selector */ DataSection}:first-child {
    padding-top: 0;
    border-top: 0;
  }
  & ${/* sc-selector */ ErrorContainer} {
    margin-bottom: ${space(2)};
  }
`;

type StyledEventUserFeedbackProps = {
  includeBorder: boolean;
};

const StyledEventUserFeedback = styled(EventUserFeedback)<StyledEventUserFeedbackProps>`
  border-radius: 0;
  box-shadow: none;
  padding: ${space(3)} ${space(4)} 0 40px;
  border: 0;
  ${p => (p.includeBorder ? `border-top: 1px solid ${p.theme.innerBorder};` : '')}
  margin: 0;
`;

const StyledEventDataSection = styled(EventDataSection)`
  margin-bottom: ${space(2)};
`;

// TODO(ts): any required due to our use of SharedViewOrganization
export default withOrganization<any>(withApi(EventEntries));
export {BorderlessEventEntries};