import { css, cx } from '@emotion/css';
import React, { useCallback, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { useAsync } from 'react-use';

import { GrafanaTheme2, DataSourceApi } from '@grafana/data';
import { config, getDataSourceSrv, reportInteraction, getAppEvents } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { TextArea, Button, IconButton, useStyles2, LoadingPlaceholder } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { createSuccessNotification } from 'app/core/copy/appNotification';
import { copyStringToClipboard } from 'app/core/utils/explore';
import { createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { changeDatasource } from 'app/features/explore/state/datasource';
import { starHistoryItem, commentHistoryItem, deleteHistoryItem } from 'app/features/explore/state/history';
import { setQueries } from 'app/features/explore/state/query';
import { dispatch } from 'app/store/store';
import { StoreState } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { RichHistoryQuery } from 'app/types/explore';

function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
  const explore = state.explore;
  const { datasourceInstance } = explore.panes[exploreId]!;
  return {
    exploreId,
    datasourceInstance,
  };
}

const mapDispatchToProps = {
  changeDatasource,
  deleteHistoryItem,
  commentHistoryItem,
  starHistoryItem,
  setQueries,
};

const connector = connect(mapStateToProps, mapDispatchToProps);

interface OwnProps<T extends DataQuery = DataQuery> {
  query: RichHistoryQuery<T>;
}

export type Props<T extends DataQuery = DataQuery> = ConnectedProps<typeof connector> & OwnProps<T>;

const getStyles = (theme: GrafanaTheme2) => {
  /* Hard-coded value so all buttons and icons on right side of card are aligned */
  const rightColumnWidth = '240px';
  const rightColumnContentWidth = '170px';

  /* If datasource was removed, card will have inactive color */
  const cardColor = theme.colors.background.secondary;

  return {
    queryCard: css`
      position: relative;
      display: flex;
      flex-direction: column;
      border: 1px solid ${theme.colors.border.weak};
      margin: ${theme.spacing(1)} 0;
      background-color: ${cardColor};
      border-radius: ${theme.shape.radius.default};
      .starred {
        color: ${theme.v1.palette.orange};
      }
    `,
    cardRow: css`
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: ${theme.spacing(1)};
      border-bottom: none;
      :first-of-type {
        border-bottom: 1px solid ${theme.colors.border.weak};
        padding: ${theme.spacing(0.5, 1)};
      }
      img {
        height: ${theme.typography.fontSize}px;
        max-width: ${theme.typography.fontSize}px;
        margin-right: ${theme.spacing(1)};
      }
    `,
    queryActionButtons: css`
      max-width: ${rightColumnContentWidth};
      display: flex;
      justify-content: flex-end;
      font-size: ${theme.typography.size.base};
      button {
        margin-left: ${theme.spacing(1)};
      }
    `,
    queryContainer: css`
      font-weight: ${theme.typography.fontWeightMedium};
      width: calc(100% - ${rightColumnWidth});
    `,
    updateCommentContainer: css`
      width: calc(100% + ${rightColumnWidth});
      margin-top: ${theme.spacing(1)};
    `,
    comment: css`
      overflow-wrap: break-word;
      font-size: ${theme.typography.bodySmall.fontSize};
      font-weight: ${theme.typography.fontWeightRegular};
      margin-top: ${theme.spacing(0.5)};
    `,
    commentButtonRow: css`
      > * {
        margin-top: ${theme.spacing(1)};
        margin-right: ${theme.spacing(1)};
      }
    `,
    textArea: css`
      width: 100%;
    `,
    runButton: css`
      max-width: ${rightColumnContentWidth};
      display: flex;
      justify-content: flex-end;
      button {
        height: auto;
        padding: ${theme.spacing(0.5, 2)};
        line-height: 1.4;
        span {
          white-space: normal !important;
        }
      }
    `,
    loader: css`
      position: absolute;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color: ${theme.colors.background.secondary};
    `,
  };
};

export function RichHistoryCard(props: Props) {
  const {
    query,
    commentHistoryItem,
    starHistoryItem,
    deleteHistoryItem,
    changeDatasource,
    exploreId,
    datasourceInstance,
    setQueries,
  } = props;
  const [activeUpdateComment, setActiveUpdateComment] = useState(false);
  const [comment, setComment] = useState<string | undefined>(query.comment);
  const { value, loading } = useAsync(async () => {
    let dsInstance: DataSourceApi | undefined;
    try {
      dsInstance = await getDataSourceSrv().get(query.datasourceUid);
    } catch (e) {}

    return {
      dsInstance,
      queries: await Promise.all(
        query.queries.map(async (query) => {
          let datasource;
          if (dsInstance?.meta.mixed) {
            try {
              datasource = await getDataSourceSrv().get(query.datasource);
            } catch (e) {}
          } else {
            datasource = dsInstance;
          }

          return {
            query,
            datasource,
          };
        })
      ),
    };
  }, [query.datasourceUid, query.queries]);

  const styles = useStyles2(getStyles);

  const onRunQuery = async () => {
    const queriesToRun = query.queries;
    const differentDataSource = query.datasourceUid !== datasourceInstance?.uid;
    if (differentDataSource) {
      await changeDatasource(exploreId, query.datasourceUid);
    }
    setQueries(exploreId, queriesToRun);

    reportInteraction('grafana_explore_query_history_run', {
      queryHistoryEnabled: config.queryHistoryEnabled,
      differentDataSource,
    });
  };

  const onCopyQuery = async () => {
    const datasources = [...query.queries.map((q) => q.datasource?.type || 'unknown')];
    reportInteraction('grafana_explore_query_history_copy_query', {
      datasources,
      mixed: Boolean(value?.dsInstance?.meta.mixed),
    });

    if (loading || !value) {
      return;
    }

    const queriesText = value.queries
      .map((q) => {
        return createQueryText(q.query, q.datasource);
      })
      .join('\n');

    copyStringToClipboard(queriesText);
    dispatch(notifyApp(createSuccessNotification('Query copied to clipboard')));
  };

  const onCreateShortLink = async () => {
    const link = createUrlFromRichHistory(query);
    await createAndCopyShortLink(link);
  };

  const onDeleteQuery = () => {
    const performDelete = (queryId: string) => {
      deleteHistoryItem(queryId);
      dispatch(notifyApp(createSuccessNotification('Query deleted')));
      reportInteraction('grafana_explore_query_history_deleted', {
        queryHistoryEnabled: config.queryHistoryEnabled,
      });
    };

    // For starred queries, we want confirmation. For non-starred, we don't.
    if (query.starred) {
      getAppEvents().publish(
        new ShowConfirmModalEvent({
          title: 'Delete',
          text: 'Are you sure you want to permanently delete your starred query?',
          yesText: 'Delete',
          icon: 'trash-alt',
          onConfirm: () => performDelete(query.id),
        })
      );
    } else {
      performDelete(query.id);
    }
  };

  const onStarrQuery = () => {
    starHistoryItem(query.id, !query.starred);
    reportInteraction('grafana_explore_query_history_starred', {
      queryHistoryEnabled: config.queryHistoryEnabled,
      newValue: !query.starred,
    });
  };

  const toggleActiveUpdateComment = () => setActiveUpdateComment(!activeUpdateComment);

  const onUpdateComment = () => {
    commentHistoryItem(query.id, comment);
    setActiveUpdateComment(false);
    reportInteraction('grafana_explore_query_history_commented', {
      queryHistoryEnabled: config.queryHistoryEnabled,
    });
  };

  const onCancelUpdateComment = () => {
    setActiveUpdateComment(false);
    setComment(query.comment);
  };

  const onKeyDown = (keyEvent: React.KeyboardEvent) => {
    if (keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) {
      onUpdateComment();
    }

    if (keyEvent.key === 'Escape') {
      onCancelUpdateComment();
    }
  };

  const updateComment = (
    <div className={styles.updateCommentContainer} aria-label={comment ? 'Update comment form' : 'Add comment form'}>
      <TextArea
        onKeyDown={onKeyDown}
        value={comment}
        placeholder={comment ? undefined : 'An optional description of what the query does.'}
        onChange={(e) => setComment(e.currentTarget.value)}
        className={styles.textArea}
      />
      <div className={styles.commentButtonRow}>
        <Button onClick={onUpdateComment}>Save comment</Button>
        <Button variant="secondary" onClick={onCancelUpdateComment}>
          Cancel
        </Button>
      </div>
    </div>
  );

  const queryActionButtons = (
    <div className={styles.queryActionButtons}>
      <IconButton
        name="comment-alt"
        onClick={toggleActiveUpdateComment}
        tooltip={query.comment?.length > 0 ? 'Edit comment' : 'Add comment'}
      />
      <IconButton name="copy" onClick={onCopyQuery} tooltip="Copy query to clipboard" />
      {value?.dsInstance && (
        <IconButton name="share-alt" onClick={onCreateShortLink} tooltip="Copy shortened link to clipboard" />
      )}
      <IconButton name="trash-alt" title="Delete query" tooltip="Delete query" onClick={onDeleteQuery} />
      <IconButton
        name={query.starred ? 'favorite' : 'star'}
        iconType={query.starred ? 'mono' : 'default'}
        onClick={onStarrQuery}
        tooltip={query.starred ? 'Unstar query' : 'Star query'}
      />
    </div>
  );

  return (
    <div className={styles.queryCard}>
      <div className={styles.cardRow}>
        <DatasourceInfo dsApi={value?.dsInstance} size="sm" />

        {queryActionButtons}
      </div>
      <div className={cx(styles.cardRow)}>
        <div className={styles.queryContainer}>
          {value?.queries.map((q, i) => {
            return <Query query={q} key={`${q}-${i}`} showDsInfo={value?.dsInstance?.meta.mixed} />;
          })}
          {!activeUpdateComment && query.comment && (
            <div aria-label="Query comment" className={styles.comment}>
              {query.comment}
            </div>
          )}
          {activeUpdateComment && updateComment}
        </div>
        {!activeUpdateComment && (
          <div className={styles.runButton}>
            <Button
              variant="secondary"
              onClick={onRunQuery}
              disabled={!value?.dsInstance || value.queries.some((query) => !query.datasource)}
            >
              {datasourceInstance?.uid === query.datasourceUid ? 'Run query' : 'Switch data source and run query'}
            </Button>
          </div>
        )}
      </div>
      {loading && <LoadingPlaceholder text="loading..." className={styles.loader} />}
    </div>
  );
}

const getQueryStyles = (theme: GrafanaTheme2) => ({
  queryRow: css`
    border-top: 1px solid ${theme.colors.border.weak};
    display: flex;
    flex-direction: row;
    padding: 4px 0px;
    gap: 4px;
    :first-child {
      border-top: none;
    }
  `,
  dsInfoContainer: css`
    display: flex;
    align-items: center;
  `,
  queryText: css`
    word-break: break-all;
  `,
});

interface QueryProps {
  query: {
    query: DataQuery;
    datasource?: DataSourceApi;
  };
  /** Show datasource info (icon+name) alongside the query text */
  showDsInfo?: boolean;
}

const Query = ({ query, showDsInfo = false }: QueryProps) => {
  const styles = useStyles2(getQueryStyles);

  return (
    <div className={styles.queryRow}>
      {showDsInfo && (
        <div className={styles.dsInfoContainer}>
          <DatasourceInfo dsApi={query.datasource} size="md" />
          {': '}
        </div>
      )}
      <span aria-label="Query text" className={styles.queryText}>
        {createQueryText(query.query, query.datasource)}
      </span>
    </div>
  );
};

const getDsInfoStyles = (size: 'sm' | 'md') => (theme: GrafanaTheme2) => css`
  display: flex;
  align-items: center;
  font-size: ${theme.typography[size === 'sm' ? 'bodySmall' : 'body'].fontSize};
  font-weight: ${theme.typography.fontWeightMedium};
  white-space: nowrap;
`;

function DatasourceInfo({ dsApi, size }: { dsApi?: DataSourceApi; size: 'sm' | 'md' }) {
  const getStyles = useCallback((theme: GrafanaTheme2) => getDsInfoStyles(size)(theme), [size]);
  const styles = useStyles2(getStyles);

  return (
    <div className={styles}>
      <img
        src={dsApi?.meta.info.logos.small || 'public/img/icn-datasource.svg'}
        alt={dsApi?.type || 'Data source does not exist anymore'}
        aria-label="Data source icon"
      />
      <div aria-label="Data source name">{dsApi?.name || 'Data source does not exist anymore'}</div>
    </div>
  );
}

export default connector(RichHistoryCard);
