import type {
  ActionFunction,
  LoaderFunction,
  V2_MetaFunction,
} from "@remix-run/node";
import { json } from "@remix-run/node";
import type { ShouldRevalidateFunction } from "@remix-run/react";
import { Link, Outlet, useLoaderData, useSearchParams } from "@remix-run/react";
import clsx from "clsx";
import invariant from "tiny-invariant";
import { z } from "zod";
import { Avatar } from "~/components/Avatar";
import { Button, LinkButton } from "~/components/Button";
import { Catcher } from "~/components/Catcher";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { TrashIcon } from "~/components/icons/Trash";
import { nextNonCompletedVoting } from "~/modules/plus-server";
import { db } from "~/db";
import type * as plusSuggestions from "~/db/models/plusSuggestions/queries.server";
import type { PlusSuggestion, User } from "~/db/types";
import { requireUser, useUser } from "~/modules/auth";
import {
  canAddCommentToSuggestionFE,
  canSuggestNewUserFE,
  canDeleteComment,
  canDeleteSuggestionOfThemselves,
  isFirstSuggestion,
} from "~/permissions";
import { parseRequestFormData, validate } from "~/utils/remix";
import { makeTitle } from "~/utils/strings";
import { discordFullName } from "~/utils/strings";
import { actualNumber } from "~/utils/zod";
import { userPage } from "~/utils/urls";
import { RelativeTime } from "~/components/RelativeTime";
import { databaseTimestampToDate } from "~/utils/dates";
import { PLUS_TIERS } from "~/constants";
import { assertUnreachable } from "~/utils/types";
import { getUserId } from "~/modules/auth/user.server";

export const meta: V2_MetaFunction = () => {
  return [
    { title: makeTitle("Plus Server suggestions") },
    {
      name: "description",
      content: "This month's suggestions for +1, +2 and +3.",
    },
  ];
};

const suggestionActionSchema = z.union([
  z.object({
    _action: z.literal("DELETE_COMMENT"),
    suggestionId: z.preprocess(actualNumber, z.number()),
  }),
  z.object({
    _action: z.literal("DELETE_SUGGESTION_OF_THEMSELVES"),
    tier: z.preprocess(
      actualNumber,
      z
        .number()
        .min(Math.min(...PLUS_TIERS))
        .max(Math.max(...PLUS_TIERS))
    ),
  }),
]);

export const action: ActionFunction = async ({ request }) => {
  const data = await parseRequestFormData({
    request,
    schema: suggestionActionSchema,
  });
  const user = await requireUser(request);

  switch (data._action) {
    case "DELETE_COMMENT": {
      const suggestions = db.plusSuggestions.findVisibleForUser({
        ...nextNonCompletedVoting(new Date()),
        plusTier: user.plusTier,
      });

      const flattenedSuggestedUserInfo = Object.entries(suggestions ?? {})
        .flatMap(([tier, suggestions]) =>
          suggestions.map(({ suggestedUser, suggestions }) => ({
            tier: Number(tier),
            suggestedUser,
            suggestions,
          }))
        )
        .find(({ suggestions }) =>
          suggestions.some((s) => s.id === data.suggestionId)
        );

      validate(suggestions);
      validate(flattenedSuggestedUserInfo);
      validate(
        canDeleteComment({
          user,
          author: flattenedSuggestedUserInfo.suggestions.find(
            (s) => s.id === data.suggestionId
          )!.author,
          suggestionId: data.suggestionId,
          suggestions,
        })
      );

      const suggestionHasComments =
        flattenedSuggestedUserInfo.suggestions.length > 1;
      if (
        suggestionHasComments &&
        isFirstSuggestion({ suggestionId: data.suggestionId, suggestions })
      ) {
        // admin only action
        db.plusSuggestions.deleteSuggestionWithComments({
          ...nextNonCompletedVoting(new Date()),
          tier: flattenedSuggestedUserInfo.tier,
          suggestedId: flattenedSuggestedUserInfo.suggestedUser.id,
        });
      } else {
        db.plusSuggestions.del(data.suggestionId);
      }

      break;
    }
    case "DELETE_SUGGESTION_OF_THEMSELVES": {
      validate(canDeleteSuggestionOfThemselves());

      db.plusSuggestions.deleteAll({ suggestedId: user.id, tier: data.tier });

      break;
    }
    default: {
      assertUnreachable(data);
    }
  }

  return null;
};

export interface PlusSuggestionsLoaderData {
  suggestions: plusSuggestions.FindVisibleForUser;
  suggestedForTiers: number[];
}

export const shouldRevalidate: ShouldRevalidateFunction = ({ formMethod }) => {
  // only reload if form submission not when user changes tabs
  return Boolean(formMethod && formMethod !== "get");
};

export const loader: LoaderFunction = async ({ request }) => {
  const user = await getUserId(request);

  return json<PlusSuggestionsLoaderData>({
    suggestions: db.plusSuggestions.findAll({
      ...nextNonCompletedVoting(new Date()),
    }),
    suggestedForTiers: user
      ? db.plusSuggestions.tiersSuggestedFor({
          ...nextNonCompletedVoting(new Date()),
          userId: user.id,
        })
      : [],
  });
};

export default function PlusSuggestionsPage() {
  const data = useLoaderData<PlusSuggestionsLoaderData>();
  const [searchParams, setSearchParams] = useSearchParams();
  const user = useUser();
  const tierVisible = searchParamsToLegalTier(searchParams, data.suggestions);

  const handleTierChange = (tier: string) => {
    setSearchParams({ tier });
  };

  const visibleSuggestions =
    tierVisible && data.suggestions[tierVisible]
      ? data.suggestions[tierVisible]
      : [];
  invariant(visibleSuggestions);

  return (
    <>
      <Outlet />
      <div className="plus__container">
        <div className="stack md">
          <SuggestedForInfo />
          <div className="stack lg">
            <div
              className={clsx("plus__top-container", {
                "content-centered": !canSuggestNewUserFE({
                  user,
                  suggestions: data.suggestions,
                }),
              })}
            >
              <div className="plus__radios">
                {Object.entries(data.suggestions)
                  .sort((a, b) => Number(a[0]) - Number(b[0]))
                  .map(([tier, suggestions]) => {
                    const id = String(tier);
                    return (
                      <div key={id} className="plus__radio-container">
                        <label htmlFor={id} className="plus__radio-label">
                          +{tier}{" "}
                          <span className="plus__users-count">
                            ({suggestions.length})
                          </span>
                        </label>
                        <input
                          id={id}
                          name="tier"
                          type="radio"
                          checked={tierVisible === tier}
                          onChange={() => handleTierChange(tier)}
                          data-cy={`plus${tier}-radio`}
                        />
                      </div>
                    );
                  })}
              </div>
              {canSuggestNewUserFE({ user, suggestions: data.suggestions }) ? (
                // TODO: resetScroll={false} https://twitter.com/ryanflorence/status/1527775882797907969
                <LinkButton
                  to={`new${tierVisible ? `?tier=${tierVisible}` : ""}`}
                  prefetch="render"
                  size="tiny"
                >
                  Suggest
                </LinkButton>
              ) : null}
            </div>
            <div className="stack lg">
              {visibleSuggestions.map((u) => {
                invariant(tierVisible);
                return (
                  <SuggestedUser
                    key={`${u.suggestedUser.id}-${tierVisible}`}
                    suggested={u}
                    tier={tierVisible}
                  />
                );
              })}
              {visibleSuggestions.length === 0 ? (
                <div className="plus__suggested-info-text text-center">
                  No suggestions yet
                </div>
              ) : null}
            </div>
          </div>
        </div>
      </div>
    </>
  );
}

function searchParamsToLegalTier(
  searchParams: URLSearchParams,
  suggestions?: plusSuggestions.FindVisibleForUser
) {
  const tierFromSearchParams = searchParams.get("tier");
  if (
    !tierFromSearchParams ||
    !suggestions ||
    !suggestions[tierFromSearchParams]
  ) {
    return tierVisibleInitialState(suggestions);
  }

  return tierFromSearchParams;
}

function tierVisibleInitialState(
  suggestions?: plusSuggestions.FindVisibleForUser
) {
  if (!suggestions || Object.keys(suggestions).length === 0) return;
  return String(Math.min(...Object.keys(suggestions).map(Number)));
}

function SuggestedForInfo() {
  const data = useLoaderData<PlusSuggestionsLoaderData>();

  if (data.suggestedForTiers.length === 0) return null;

  return (
    <div className="stack md">
      {canDeleteSuggestionOfThemselves() ? (
        <div className="stack horizontal md">
          {data.suggestedForTiers.map((tier) => (
            <FormWithConfirm
              key={tier}
              fields={[
                ["_action", "DELETE_SUGGESTION_OF_THEMSELVES"],
                ["tier", tier],
              ]}
              dialogHeading={`Delete your suggestion to +${tier}? You won't appear in next voting.`}
            >
              <Button
                key={tier}
                size="tiny"
                variant="destructive"
                type="submit"
              >
                Delete your +{tier} suggestion
              </Button>
            </FormWithConfirm>
          ))}
        </div>
      ) : null}
    </div>
  );
}

function SuggestedUser({
  suggested,
  tier,
}: {
  suggested: plusSuggestions.FindVisibleForUserSuggestedUserInfo;
  tier: string;
}) {
  const data = useLoaderData<PlusSuggestionsLoaderData>();
  const user = useUser();

  invariant(data.suggestions);

  return (
    <div className="stack md">
      <div className="plus__suggested-user-info">
        <Avatar user={suggested.suggestedUser} size="md" />
        <h2>
          <Link className="all-unset" to={userPage(suggested.suggestedUser)}>
            {suggested.suggestedUser.discordName}
          </Link>
        </h2>
        {canAddCommentToSuggestionFE({
          user,
          suggestions: data.suggestions,
          suggested: { id: suggested.suggestedUser.id },
          targetPlusTier: Number(tier),
        }) ? (
          // TODO: resetScroll={false} https://twitter.com/ryanflorence/status/1527775882797907969
          <LinkButton
            className="plus__comment-button"
            size="tiny"
            variant="outlined"
            to={`comment/${tier}/${suggested.suggestedUser.id}?tier=${tier}`}
            prefetch="render"
          >
            Comment
          </LinkButton>
        ) : null}
      </div>
      <PlusSuggestionComments
        suggestions={suggested.suggestions}
        deleteButtonArgs={{
          suggested,
          user,
          tier,
          suggestions: data.suggestions,
        }}
      />
    </div>
  );
}

export function PlusSuggestionComments({
  suggestions,
  deleteButtonArgs,
  defaultOpen,
}: {
  suggestions: plusSuggestions.FindVisibleForUserSuggestedUserInfo["suggestions"];
  deleteButtonArgs?: {
    user?: Pick<User, "id" | "discordId">;
    suggestions: plusSuggestions.FindVisibleForUser;
    tier: string;
    suggested: plusSuggestions.FindVisibleForUserSuggestedUserInfo;
  };
  defaultOpen?: true;
}) {
  return (
    <details open={defaultOpen} className="w-full">
      <summary className="plus__view-comments-action">
        Comments ({suggestions.length})
      </summary>
      <div className="stack sm mt-2">
        {suggestions.map((suggestion) => {
          return (
            <fieldset key={suggestion.id} className="plus__comment">
              <legend>{discordFullName(suggestion.author)}</legend>
              {suggestion.text}
              <div className="stack horizontal xs items-center">
                <span className="plus__comment-time">
                  <RelativeTime
                    timestamp={databaseTimestampToDate(
                      suggestion.createdAt
                    ).getTime()}
                  >
                    {suggestion.createdAtRelative}
                  </RelativeTime>
                </span>
                {deleteButtonArgs &&
                canDeleteComment({
                  author: suggestion.author,
                  user: deleteButtonArgs.user,
                  suggestionId: suggestion.id,
                  suggestions: deleteButtonArgs.suggestions,
                }) ? (
                  <CommentDeleteButton
                    suggestionId={suggestion.id}
                    tier={deleteButtonArgs.tier}
                    suggestedDiscordName={
                      deleteButtonArgs.suggested.suggestedUser.discordName
                    }
                    isFirstSuggestion={
                      deleteButtonArgs.suggested.suggestions.length === 1
                    }
                  />
                ) : null}
              </div>
            </fieldset>
          );
        })}
      </div>
    </details>
  );
}

function CommentDeleteButton({
  suggestionId,
  tier,
  suggestedDiscordName,
  isFirstSuggestion = false,
}: {
  suggestionId: PlusSuggestion["id"];
  tier: string;
  suggestedDiscordName: string;
  isFirstSuggestion?: boolean;
}) {
  return (
    <FormWithConfirm
      fields={[
        ["suggestionId", suggestionId],
        ["_action", "DELETE_COMMENT"],
      ]}
      dialogHeading={
        isFirstSuggestion
          ? `Delete your suggestion of ${suggestedDiscordName} to +${tier}?`
          : `Delete your comment to ${suggestedDiscordName}'s +${tier} suggestion?`
      }
    >
      <Button
        className="plus__delete-button"
        icon={<TrashIcon />}
        variant="minimal-destructive"
        aria-label="Delete comment"
      />
    </FormWithConfirm>
  );
}

export const CatchBoundary = Catcher;
