import { orderBy } from "lodash";
import moment from "moment";
import * as numeral from "numeral";
import React from "react";
import { connect } from "react-redux";
import sanitize from "sanitize-html";

import {
  AnchorButton,
  Button,
  Callout,
  Classes,
  Code,
  Divider,
  H2,
  HTMLTable,
  Icon,
  NonIdealState,
  Position,
  Spinner,
  Tab,
  Tabs,
  Tooltip
} from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";

import { push } from "connected-react-router";
import { Link } from "react-router-dom";
import { Dispatch } from "redux";
import styled from "styled-components";
import { IAppState, IGraph, IInstanceDetails } from "../../redux/types";
import { domainMatchSelector, getFromApi, isSmallScreen } from "../../util";
import { Cytoscape, ErrorState } from "../molecules/";

const InstanceScreenContainer = styled.div`
  margin-bottom: auto;
  display: flex;
  flex-direction: column;
  flex: 1;
`;
const HeadingContainer = styled.div`
  display: flex;
  flex-direction: row;
  align-items: center;
  width: 100%;
  padding: 0 20px;
`;
const StyledHeadingH2 = styled(H2)`
  margin: 0;
`;
const StyledCloseButton = styled(Button)`
  justify-self: flex-end;
`;
const StyledHeadingTooltip = styled(Tooltip)`
  margin-left: 5px;
  flex-grow: 1;
`;
const StyledHTMLTable = styled(HTMLTable)`
  width: 100%;
`;
const StyledLinkToFdNetwork = styled.div`
  text-align: center;
  margin-top: auto;
`;
const StyledCallout = styled(Callout)`
  margin: 10px 20px;
  width: auto;
`;
const StyledTabs = styled(Tabs)`
  width: 100%;
  padding: 0 20px;
`;
const StyledGraphContainer = styled.div`
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
`;
interface IInstanceScreenProps {
  graph?: IGraph;
  instanceName: string | null;
  instanceLoadError: boolean;
  instanceDetails: IInstanceDetails | null;
  isLoadingInstanceDetails: boolean;
  navigateToRoot: () => void;
  navigateToInstance: (domain: string) => void;
}
interface IInstanceScreenState {
  neighbors?: string[];
  isProcessingNeighbors: boolean;
  // Local (neighborhood) graph. Used only on small screens (mobile devices).
  isLoadingLocalGraph: boolean;
  localGraph?: IGraph;
  localGraphLoadError?: boolean;
}
class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInstanceScreenState> {
  public constructor(props: IInstanceScreenProps) {
    super(props);
    this.state = { isProcessingNeighbors: false, isLoadingLocalGraph: false, localGraphLoadError: false };
  }

  public render() {
    let content;
    if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors || this.state.isLoadingLocalGraph) {
      content = this.renderLoadingState();
    } else if (this.props.instanceLoadError || this.state.localGraphLoadError || !this.props.instanceDetails) {
      return (content = <ErrorState />);
    } else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
      content = this.renderPersonalInstanceErrorState();
    } else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) {
      content = this.renderRobotsTxtState();
    } else if (this.props.instanceDetails.status !== "success") {
      content = this.renderMissingDataState();
    } else {
      content = this.renderTabs();
    }
    return (
      <InstanceScreenContainer>
        <HeadingContainer>
          <StyledHeadingH2>{this.props.instanceName}</StyledHeadingH2>
          <StyledHeadingTooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
            <AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
          </StyledHeadingTooltip>
          <StyledCloseButton icon={IconNames.CROSS} onClick={this.props.navigateToRoot} />
        </HeadingContainer>
        <Divider />
        {content}
      </InstanceScreenContainer>
    );
  }

  public componentDidMount() {
    this.loadLocalGraphOnSmallScreen();
    this.processEdgesToFindNeighbors();
  }

  public componentDidUpdate(prevProps: IInstanceScreenProps, prevState: IInstanceScreenState) {
    const isNewInstance = prevProps.instanceName !== this.props.instanceName;
    const receivedNewEdges = !!this.props.graph && !this.state.isProcessingNeighbors && !this.state.neighbors;
    const receivedNewLocalGraph = !!this.state.localGraph && !prevState.localGraph;
    if (isNewInstance || receivedNewEdges || receivedNewLocalGraph) {
      this.processEdgesToFindNeighbors();
    }
  }

  private processEdgesToFindNeighbors = () => {
    const { graph, instanceName } = this.props;
    const { localGraph } = this.state;
    if ((!graph && !localGraph) || !instanceName) {
      return;
    }
    this.setState({ isProcessingNeighbors: true });

    const graphToUse = !!graph ? graph : localGraph;
    const edges = graphToUse!.edges.filter(e => [e.data.source, e.data.target].indexOf(instanceName!) > -1);
    const neighbors: any[] = [];
    edges.forEach(e => {
      if (e.data.source === instanceName) {
        neighbors.push({ neighbor: e.data.target, weight: e.data.weight });
      } else {
        neighbors.push({ neighbor: e.data.source, weight: e.data.weight });
      }
    });
    this.setState({ neighbors, isProcessingNeighbors: false });
  };

  private loadLocalGraphOnSmallScreen = () => {
    if (!isSmallScreen) {
      return;
    }
    this.setState({ isLoadingLocalGraph: true });
    getFromApi(`graph/${this.props.instanceName}`)
      .then((response: IGraph) => {
        // We do some processing of edges here to make sure that every edge's source and target are in the neighborhood
        // We could (and should) be doing this in the backend, but I don't want to mess around with complex SQL
        // queries.
        // TODO: think more about moving the backend to a graph database that would make this easier.
        const nodeIds = new Set(response.nodes.map(n => n.data.id));
        const edges = response.edges.filter(e => nodeIds.has(e.data.source) && nodeIds.has(e.data.target));
        this.setState({ isLoadingLocalGraph: false, localGraph: { ...response, edges } });
      })
      .catch(() => this.setState({ isLoadingLocalGraph: false, localGraphLoadError: true }));
  };

  private renderTabs = () => {
    const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;

    const hasLocalGraph =
      !!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0;
    const insularCallout =
      this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors && !hasLocalGraph ? (
        <StyledCallout icon={IconNames.INFO_SIGN} title="Insular instance">
          <p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
        </StyledCallout>
      ) : (
        undefined
      );
    return (
      <>
        {insularCallout}
        {this.maybeRenderLocalGraph()}
        <StyledTabs>
          {this.props.instanceDetails!.description && (
            <Tab id="description" title="Description" panel={this.renderDescription()} />
          )}
          {this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
          <Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
          <Tab id="peers" title="Known peers" panel={this.renderPeers()} />
        </StyledTabs>
        <StyledLinkToFdNetwork>
          <AnchorButton
            href={`https://fediverse.network/${this.props.instanceName}`}
            minimal={true}
            rightIcon={IconNames.SHARE}
            target="_blank"
            text="See more statistics at fediverse.network"
          />
        </StyledLinkToFdNetwork>
      </>
    );
  };

  private maybeRenderLocalGraph = () => {
    const { localGraph } = this.state;
    const hasLocalGraph =
      !!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0;
    if (!hasLocalGraph) {
      return;
    }
    return (
      <StyledGraphContainer>
        <Cytoscape
          elements={localGraph!}
          currentNodeId={this.props.instanceName}
          navigateToInstancePath={this.props.navigateToInstance}
        />
        <Divider />
      </StyledGraphContainer>
    );
  };

  private shouldRenderStats = () => {
    const details = this.props.instanceDetails;
    return details && (details.version || details.userCount || details.statusCount || details.domainCount);
  };

  private renderDescription = () => {
    const description = this.props.instanceDetails!.description;
    if (!description) {
      return;
    }
    return <p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{ __html: sanitize(description) }} />;
  };

  private renderVersionAndCounts = () => {
    if (!this.props.instanceDetails) {
      throw new Error("Did not receive instance details as expected!");
    }
    const { version, userCount, statusCount, domainCount, lastUpdated, insularity } = this.props.instanceDetails;
    return (
      <StyledHTMLTable small={true} striped={true}>
        <tbody>
          <tr>
            <td>Version</td>
            <td>{<Code>{version}</Code> || "Unknown"}</td>
          </tr>
          <tr>
            <td>Users</td>
            <td>{(userCount && numeral.default(userCount).format("0,0")) || "Unknown"}</td>
          </tr>
          <tr>
            <td>Statuses</td>
            <td>{(statusCount && numeral.default(statusCount).format("0,0")) || "Unknown"}</td>
          </tr>
          <tr>
            <td>
              Insularity{" "}
              <Tooltip
                content={
                  <span>
                    The percentage of mentions that are directed
                    <br />
                    toward users on the same instance.
                  </span>
                }
                position={Position.TOP}
                className={Classes.DARK}
              >
                <Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
              </Tooltip>
            </td>
            <td>{(insularity && numeral.default(insularity).format("0.0%")) || "Unknown"}</td>
          </tr>
          <tr>
            <td>Known peers</td>
            <td>{(domainCount && numeral.default(domainCount).format("0,0")) || "Unknown"}</td>
          </tr>
          <tr>
            <td>Last updated</td>
            <td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
          </tr>
        </tbody>
      </StyledHTMLTable>
    );
  };

  private renderNeighbors = () => {
    if (!this.state.neighbors) {
      return;
    }
    const neighborRows = orderBy(this.state.neighbors, ["weight"], ["desc"]).map(
      (neighborDetails: any, idx: number) => (
        <tr key={idx}>
          <td>
            <Link
              to={`/instance/${neighborDetails.neighbor}`}
              className={`${Classes.BUTTON} ${Classes.MINIMAL}`}
              role="button"
            >
              {neighborDetails.neighbor}
            </Link>
          </td>
          <td>{neighborDetails.weight.toFixed(4)}</td>
        </tr>
      )
    );
    return (
      <div>
        <p className={Classes.TEXT_MUTED}>
          The mention ratio is the average of how many times the two instances mention each other per status. A mention
          ratio of 1 would mean that every single status contained a mention of a user on the other instance.
        </p>
        <StyledHTMLTable small={true} striped={true} interactive={false}>
          <thead>
            <tr>
              <th>Instance</th>
              <th>Mention ratio</th>
            </tr>
          </thead>
          <tbody>{neighborRows}</tbody>
        </StyledHTMLTable>
      </div>
    );
  };

  private renderPeers = () => {
    const peers = this.props.instanceDetails!.peers;
    if (!peers || peers.length === 0) {
      return;
    }
    const peerRows = peers.map(instance => (
      <tr key={instance.name}>
        <td>
          <Link to={`/instance/${instance.name}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
            {instance.name}
          </Link>
        </td>
      </tr>
    ));
    return (
      <div>
        <p className={Classes.TEXT_MUTED}>
          All the instances, past and present, that {this.props.instanceName} knows about.
        </p>
        <StyledHTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
          <tbody>{peerRows}</tbody>
        </StyledHTMLTable>
      </div>
    );
  };

  private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;

  private renderPersonalInstanceErrorState = () => {
    return (
      <NonIdealState
        icon={IconNames.BLOCKED_PERSON}
        title="No data"
        description="This instance has fewer than 10 users. It was not crawled in order to protect their privacy, but if it's your instance you can opt in."
        action={
          <AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@fediversespace" target="_blank">
            Message @fediversespace to opt in
          </AnchorButton>
        }
      />
    );
  };

  private renderMissingDataState = () => {
    return (
      <>
        <NonIdealState
          icon={IconNames.ERROR}
          title="No data"
          description="This instance could not be crawled. Either it was down or it's an instance type we don't support yet."
        />
        <span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
          {this.props.instanceDetails && this.props.instanceDetails.status}
        </span>
      </>
    );
  };

  private renderRobotsTxtState = () => {
    return (
      <NonIdealState
        icon={
          <span role="img" aria-label="robot">
            🤖
          </span>
        }
        title="No data"
        description="This instance was not crawled because its robots.txt did not allow us to."
      />
    );
  };

  private openInstanceLink = () => {
    window.open("https://" + this.props.instanceName, "_blank");
  };
}

const mapStateToProps = (state: IAppState) => {
  const match = domainMatchSelector(state);
  return {
    graph: state.data.graph,
    instanceDetails: state.currentInstance.currentInstanceDetails,
    instanceLoadError: state.currentInstance.error,
    instanceName: match && match.params.domain,
    isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
  };
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
  navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
  navigateToRoot: () => dispatch(push("/"))
});
const InstanceScreen = connect(
  mapStateToProps,
  mapDispatchToProps
)(InstanceScreenImpl);
export default InstanceScreen;
