import { Form, Col, Row, Button, FormControl } from "react-bootstrap";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import * as GQL from "src/core/generated-graphql";
import {
  GallerySelect,
  Icon,
  LoadingIndicator,
  Modal,
  SceneSelect,
  StringListSelect,
} from "src/components/Shared";
import { FormUtils, ImageUtils, TextUtils } from "src/utils";
import { mutateSceneMerge, queryFindScenesByID } from "src/core/StashService";
import { FormattedMessage, useIntl } from "react-intl";
import { useToast } from "src/hooks";
import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
import {
  hasScrapedValues,
  ScrapeDialog,
  ScrapeDialogRow,
  ScrapedImageRow,
  ScrapedInputGroupRow,
  ScrapedTextAreaRow,
  ScrapeResult,
} from "../Shared/ScrapeDialog";
import { clone, uniq } from "lodash-es";
import {
  ScrapedMoviesRow,
  ScrapedPerformersRow,
  ScrapedStudioRow,
  ScrapedTagsRow,
} from "./SceneDetails/SceneScrapeDialog";
import { galleryTitle } from "src/core/galleries";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";

interface IStashIDsField {
  values: GQL.StashId[];
}

const StashIDsField: React.FC<IStashIDsField> = ({ values }) => {
  return <StringListSelect value={values.map((v) => v.stash_id)} />;
};

interface ISceneMergeDetailsProps {
  sources: GQL.SlimSceneDataFragment[];
  dest: GQL.SlimSceneDataFragment;
  onClose: (values?: GQL.SceneUpdateInput) => void;
}

const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
  sources,
  dest,
  onClose,
}) => {
  const intl = useIntl();

  const [loading, setLoading] = useState(true);

  const [title, setTitle] = useState<ScrapeResult<string>>(
    new ScrapeResult<string>(dest.title)
  );
  const [url, setURL] = useState<ScrapeResult<string>>(
    new ScrapeResult<string>(dest.url)
  );
  const [date, setDate] = useState<ScrapeResult<string>>(
    new ScrapeResult<string>(dest.date)
  );

  const [rating, setRating] = useState(
    new ScrapeResult<number>(dest.rating100)
  );
  const [oCounter, setOCounter] = useState(
    new ScrapeResult<number>(dest.o_counter)
  );
  const [playCount, setPlayCount] = useState(
    new ScrapeResult<number>(dest.play_count)
  );
  const [playDuration, setPlayDuration] = useState(
    new ScrapeResult<number>(dest.play_duration)
  );

  const [studio, setStudio] = useState<ScrapeResult<string>>(
    new ScrapeResult<string>(dest.studio?.id)
  );

  function sortIdList(idList?: string[] | null) {
    if (!idList) {
      return;
    }

    const ret = clone(idList);
    // sort by id numerically
    ret.sort((a, b) => {
      return parseInt(a, 10) - parseInt(b, 10);
    });

    return ret;
  }

  const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
    new ScrapeResult<string[]>(sortIdList(dest.performers.map((p) => p.id)))
  );

  const [movies, setMovies] = useState<ScrapeResult<string[]>>(
    new ScrapeResult<string[]>(sortIdList(dest.movies.map((p) => p.movie.id)))
  );

  const [tags, setTags] = useState<ScrapeResult<string[]>>(
    new ScrapeResult<string[]>(sortIdList(dest.tags.map((t) => t.id)))
  );

  const [details, setDetails] = useState<ScrapeResult<string>>(
    new ScrapeResult<string>(dest.details)
  );

  const [galleries, setGalleries] = useState<ScrapeResult<string[]>>(
    new ScrapeResult<string[]>(sortIdList(dest.galleries.map((p) => p.id)))
  );

  const [stashIDs, setStashIDs] = useState(new ScrapeResult<GQL.StashId[]>([]));

  const [image, setImage] = useState<ScrapeResult<string>>(
    new ScrapeResult<string>(dest.paths.screenshot)
  );

  // calculate the values for everything
  // uses the first set value for single value fields, and combines all
  useEffect(() => {
    async function loadImages() {
      const src = sources.find((s) => s.paths.screenshot);
      if (!dest.paths.screenshot || !src) return;

      setLoading(true);

      const destData = await ImageUtils.imageToDataURL(dest.paths.screenshot);
      const srcData = await ImageUtils.imageToDataURL(src.paths!.screenshot!);

      // keep destination image by default
      const useNewValue = false;
      setImage(new ScrapeResult(destData, srcData, useNewValue));

      setLoading(false);
    }

    // append dest to all so that if dest has stash_ids with the same
    // endpoint, then it will be excluded first
    const all = sources.concat(dest);

    setTitle(
      new ScrapeResult(
        dest.title,
        sources.find((s) => s.title)?.title,
        !dest.title
      )
    );
    setURL(
      new ScrapeResult(dest.url, sources.find((s) => s.url)?.url, !dest.url)
    );
    setDate(
      new ScrapeResult(dest.date, sources.find((s) => s.date)?.date, !dest.date)
    );
    setStudio(
      new ScrapeResult(
        dest.studio?.id,
        sources.find((s) => s.studio)?.studio?.id,
        !dest.studio
      )
    );

    setPerformers(
      new ScrapeResult(
        dest.performers.map((p) => p.id),
        uniq(all.map((s) => s.performers.map((p) => p.id)).flat())
      )
    );
    setTags(
      new ScrapeResult(
        dest.tags.map((p) => p.id),
        uniq(all.map((s) => s.tags.map((p) => p.id)).flat())
      )
    );
    setDetails(
      new ScrapeResult(
        dest.details,
        sources.find((s) => s.details)?.details,
        !dest.details
      )
    );

    setMovies(
      new ScrapeResult(
        dest.movies.map((m) => m.movie.id),
        uniq(all.map((s) => s.movies.map((m) => m.movie.id)).flat())
      )
    );

    setGalleries(
      new ScrapeResult(
        dest.galleries.map((p) => p.id),
        uniq(all.map((s) => s.galleries.map((p) => p.id)).flat())
      )
    );

    setRating(
      new ScrapeResult(
        dest.rating100,
        sources.find((s) => s.rating100)?.rating100,
        !dest.rating100
      )
    );

    setOCounter(
      new ScrapeResult(
        dest.o_counter ?? 0,
        all.map((s) => s.o_counter ?? 0).reduce((pv, cv) => pv + cv, 0)
      )
    );

    setPlayCount(
      new ScrapeResult(
        dest.play_count ?? 0,
        all.map((s) => s.play_count ?? 0).reduce((pv, cv) => pv + cv, 0)
      )
    );

    setPlayDuration(
      new ScrapeResult(
        dest.play_duration ?? 0,
        all.map((s) => s.play_duration ?? 0).reduce((pv, cv) => pv + cv, 0)
      )
    );

    setStashIDs(
      new ScrapeResult(
        dest.stash_ids,
        all
          .map((s) => s.stash_ids)
          .flat()
          .filter((s, index, a) => {
            // remove entries with duplicate endpoints
            return index === a.findIndex((ss) => ss.endpoint === s.endpoint);
          }),
        !dest.stash_ids.length
      )
    );

    loadImages();
  }, [sources, dest]);

  const convertGalleries = useCallback(
    (ids?: string[]) => {
      const all = [dest, ...sources];
      return ids
        ?.map((g) =>
          all
            .map((s) => s.galleries)
            .flat()
            .find((gg) => g === gg.id)
        )
        .map((g) => {
          return {
            id: g!.id,
            title: galleryTitle(g!),
          };
        });
    },
    [dest, sources]
  );

  const originalGalleries = useMemo(() => {
    return convertGalleries(galleries.originalValue);
  }, [galleries, convertGalleries]);

  const newGalleries = useMemo(() => {
    return convertGalleries(galleries.newValue);
  }, [galleries, convertGalleries]);

  // ensure this is updated if fields are changed
  const hasValues = useMemo(() => {
    return hasScrapedValues([
      title,
      url,
      date,
      rating,
      oCounter,
      galleries,
      studio,
      performers,
      movies,
      tags,
      details,
      stashIDs,
      image,
    ]);
  }, [
    title,
    url,
    date,
    rating,
    oCounter,
    galleries,
    studio,
    performers,
    movies,
    tags,
    details,
    stashIDs,
    image,
  ]);

  function renderScrapeRows() {
    if (loading) {
      return (
        <div>
          <LoadingIndicator />
        </div>
      );
    }

    if (!hasValues) {
      return (
        <div>
          <FormattedMessage id="dialogs.merge.empty_results" />
        </div>
      );
    }

    return (
      <>
        <ScrapedInputGroupRow
          title={intl.formatMessage({ id: "title" })}
          result={title}
          onChange={(value) => setTitle(value)}
        />
        <ScrapedInputGroupRow
          title={intl.formatMessage({ id: "url" })}
          result={url}
          onChange={(value) => setURL(value)}
        />
        <ScrapedInputGroupRow
          title={intl.formatMessage({ id: "date" })}
          placeholder="YYYY-MM-DD"
          result={date}
          onChange={(value) => setDate(value)}
        />
        <ScrapeDialogRow
          title={intl.formatMessage({ id: "rating" })}
          result={rating}
          renderOriginalField={() => (
            <RatingSystem value={rating.originalValue} disabled />
          )}
          renderNewField={() => (
            <RatingSystem value={rating.newValue} disabled />
          )}
          onChange={(value) => setRating(value)}
        />
        <ScrapeDialogRow
          title={intl.formatMessage({ id: "o_counter" })}
          result={oCounter}
          renderOriginalField={() => (
            <FormControl
              value={oCounter.originalValue ?? 0}
              readOnly
              onChange={() => {}}
              className="bg-secondary text-white border-secondary"
            />
          )}
          renderNewField={() => (
            <FormControl
              value={oCounter.newValue ?? 0}
              readOnly
              onChange={() => {}}
              className="bg-secondary text-white border-secondary"
            />
          )}
          onChange={(value) => setOCounter(value)}
        />
        <ScrapeDialogRow
          title={intl.formatMessage({ id: "play_count" })}
          result={playCount}
          renderOriginalField={() => (
            <FormControl
              value={playCount.originalValue ?? 0}
              readOnly
              onChange={() => {}}
              className="bg-secondary text-white border-secondary"
            />
          )}
          renderNewField={() => (
            <FormControl
              value={playCount.newValue ?? 0}
              readOnly
              onChange={() => {}}
              className="bg-secondary text-white border-secondary"
            />
          )}
          onChange={(value) => setPlayCount(value)}
        />
        <ScrapeDialogRow
          title={intl.formatMessage({ id: "play_duration" })}
          result={playDuration}
          renderOriginalField={() => (
            <FormControl
              value={TextUtils.secondsToTimestamp(
                playDuration.originalValue ?? 0
              )}
              readOnly
              onChange={() => {}}
              className="bg-secondary text-white border-secondary"
            />
          )}
          renderNewField={() => (
            <FormControl
              value={TextUtils.secondsToTimestamp(playDuration.newValue ?? 0)}
              readOnly
              onChange={() => {}}
              className="bg-secondary text-white border-secondary"
            />
          )}
          onChange={(value) => setPlayDuration(value)}
        />
        <ScrapeDialogRow
          title={intl.formatMessage({ id: "galleries" })}
          result={galleries}
          renderOriginalField={() => (
            <GallerySelect
              className="form-control react-select"
              selected={originalGalleries ?? []}
              onSelect={() => {}}
              disabled
            />
          )}
          renderNewField={() => (
            <GallerySelect
              className="form-control react-select"
              selected={newGalleries ?? []}
              onSelect={() => {}}
              disabled
            />
          )}
          onChange={(value) => setGalleries(value)}
        />
        <ScrapedStudioRow
          title={intl.formatMessage({ id: "studios" })}
          result={studio}
          onChange={(value) => setStudio(value)}
        />
        <ScrapedPerformersRow
          title={intl.formatMessage({ id: "performers" })}
          result={performers}
          onChange={(value) => setPerformers(value)}
        />
        <ScrapedMoviesRow
          title={intl.formatMessage({ id: "movies" })}
          result={movies}
          onChange={(value) => setMovies(value)}
        />
        <ScrapedTagsRow
          title={intl.formatMessage({ id: "tags" })}
          result={tags}
          onChange={(value) => setTags(value)}
        />
        <ScrapedTextAreaRow
          title={intl.formatMessage({ id: "details" })}
          result={details}
          onChange={(value) => setDetails(value)}
        />
        <ScrapeDialogRow
          title={intl.formatMessage({ id: "stash_id" })}
          result={stashIDs}
          renderOriginalField={() => (
            <StashIDsField values={stashIDs?.originalValue ?? []} />
          )}
          renderNewField={() => (
            <StashIDsField values={stashIDs?.newValue ?? []} />
          )}
          onChange={(value) => setStashIDs(value)}
        />
        <ScrapedImageRow
          title={intl.formatMessage({ id: "cover_image" })}
          className="scene-cover"
          result={image}
          onChange={(value) => setImage(value)}
        />
      </>
    );
  }

  function createValues(): GQL.SceneUpdateInput {
    const all = [dest, ...sources];

    // only set the cover image if it's different from the existing cover image
    const coverImage = image.useNewValue ? image.getNewValue() : undefined;

    return {
      id: dest.id,
      title: title.getNewValue(),
      url: url.getNewValue(),
      date: date.getNewValue(),
      rating100: rating.getNewValue(),
      o_counter: oCounter.getNewValue(),
      play_count: playCount.getNewValue(),
      play_duration: playDuration.getNewValue(),
      gallery_ids: galleries.getNewValue(),
      studio_id: studio.getNewValue(),
      performer_ids: performers.getNewValue(),
      movies: movies.getNewValue()?.map((m) => {
        // find the equivalent movie in the original scenes
        const found = all
          .map((s) => s.movies)
          .flat()
          .find((mm) => mm.movie.id === m);
        return {
          movie_id: m,
          scene_index: found!.scene_index,
        };
      }),
      tag_ids: tags.getNewValue(),
      details: details.getNewValue(),
      stash_ids: stashIDs.getNewValue(),
      cover_image: coverImage,
    };
  }

  const dialogTitle = intl.formatMessage({
    id: "actions.merge",
  });

  const destinationLabel = !hasValues
    ? ""
    : intl.formatMessage({ id: "dialogs.merge.destination" });
  const sourceLabel = !hasValues
    ? ""
    : intl.formatMessage({ id: "dialogs.merge.source" });

  return (
    <ScrapeDialog
      title={dialogTitle}
      existingLabel={destinationLabel}
      scrapedLabel={sourceLabel}
      renderScrapeRows={renderScrapeRows}
      onClose={(apply) => {
        if (!apply) {
          onClose();
        } else {
          onClose(createValues());
        }
      }}
    />
  );
};

interface ISceneMergeModalProps {
  show: boolean;
  onClose: (mergedID?: string) => void;
  scenes: { id: string; title: string }[];
}

export const SceneMergeModal: React.FC<ISceneMergeModalProps> = ({
  show,
  onClose,
  scenes,
}) => {
  const [sourceScenes, setSourceScenes] = useState<
    { id: string; title: string }[]
  >([]);
  const [destScene, setDestScene] = useState<{ id: string; title: string }[]>(
    []
  );

  const [loadedSources, setLoadedSources] = useState<
    GQL.SlimSceneDataFragment[]
  >([]);
  const [loadedDest, setLoadedDest] = useState<GQL.SlimSceneDataFragment>();

  const [running, setRunning] = useState(false);
  const [secondStep, setSecondStep] = useState(false);

  const intl = useIntl();
  const Toast = useToast();

  const title = intl.formatMessage({
    id: "actions.merge",
  });

  useEffect(() => {
    if (scenes.length > 0) {
      // set the first scene as the destination, others as source
      setDestScene([scenes[0]]);

      if (scenes.length > 1) {
        setSourceScenes(scenes.slice(1));
      }
    }
  }, [scenes]);

  async function loadScenes() {
    const sceneIDs = sourceScenes.map((s) => parseInt(s.id));
    sceneIDs.push(parseInt(destScene[0].id));
    const query = await queryFindScenesByID(sceneIDs);
    const { scenes: loadedScenes } = query.data.findScenes;

    setLoadedDest(loadedScenes.find((s) => s.id === destScene[0].id));
    setLoadedSources(loadedScenes.filter((s) => s.id !== destScene[0].id));
    setSecondStep(true);
  }

  async function onMerge(values: GQL.SceneUpdateInput) {
    try {
      setRunning(true);
      const result = await mutateSceneMerge(
        destScene[0].id,
        sourceScenes.map((s) => s.id),
        values
      );
      if (result.data?.sceneMerge) {
        Toast.success({
          content: intl.formatMessage({ id: "toast.merged_scenes" }),
        });
        // refetch the scene
        await queryFindScenesByID([parseInt(destScene[0].id)]);
        onClose(destScene[0].id);
      }
      onClose();
    } catch (e) {
      Toast.error(e);
    } finally {
      setRunning(false);
    }
  }

  function canMerge() {
    return sourceScenes.length > 0 && destScene.length !== 0;
  }

  function switchScenes() {
    if (sourceScenes.length && destScene.length) {
      const newDest = sourceScenes[0];
      setSourceScenes([...sourceScenes.slice(1), destScene[0]]);
      setDestScene([newDest]);
    }
  }

  if (secondStep && destScene.length > 0) {
    return (
      <SceneMergeDetails
        sources={loadedSources}
        dest={loadedDest!}
        onClose={(values) => {
          if (values) {
            onMerge(values);
          } else {
            onClose();
          }
        }}
      />
    );
  }

  return (
    <Modal
      show={show}
      header={title}
      icon={faSignInAlt}
      accept={{
        text: intl.formatMessage({ id: "actions.next_action" }),
        onClick: () => loadScenes(),
      }}
      disabled={!canMerge()}
      cancel={{
        variant: "secondary",
        onClick: () => onClose(),
      }}
      isRunning={running}
    >
      <div className="form-container row px-3">
        <div className="col-12 col-lg-6 col-xl-12">
          <Form.Group controlId="source" as={Row}>
            {FormUtils.renderLabel({
              title: intl.formatMessage({ id: "dialogs.merge.source" }),
              labelProps: {
                column: true,
                sm: 3,
                xl: 12,
              },
            })}
            <Col sm={9} xl={12}>
              <SceneSelect
                isMulti
                onSelect={(items) => setSourceScenes(items)}
                selected={sourceScenes}
              />
            </Col>
          </Form.Group>
          <Form.Group
            controlId="switch"
            as={Row}
            className="justify-content-center"
          >
            <Button
              variant="secondary"
              onClick={() => switchScenes()}
              disabled={!sourceScenes.length || !destScene.length}
              title={intl.formatMessage({ id: "actions.swap" })}
            >
              <Icon className="fa-fw" icon={faExchangeAlt} />
            </Button>
          </Form.Group>
          <Form.Group controlId="destination" as={Row}>
            {FormUtils.renderLabel({
              title: intl.formatMessage({
                id: "dialogs.merge.destination",
              }),
              labelProps: {
                column: true,
                sm: 3,
                xl: 12,
              },
            })}
            <Col sm={9} xl={12}>
              <SceneSelect
                onSelect={(items) => setDestScene(items)}
                selected={destScene}
              />
            </Col>
          </Form.Group>
        </div>
      </div>
    </Modal>
  );
};
