import { assign, setup } from "xstate";

import type {
  PrivateZonePostsResponse,
  PrivateZonePostUnlockResponse,
  PrivateZoneVideoPost,
} from "@/data/schemas/private-zone";

import {
  loadPrivateZonePostsActor,
  unlockPrivateZonePostActor,
} from "./private-zone-post-actors";
import type { PrivateZonePostEvent } from "./private-zone-post-events";
import {
  createInitialPrivateZonePostState,
  type PrivateZonePostState,
} from "./private-zone-post-state";

function errorMessage(error: unknown): string {
  return error instanceof Error && error.message
    ? error.message
    : "Moments are temporarily unavailable. Please try again.";
}

function pendingPost(context: PrivateZonePostState): PrivateZoneVideoPost | null {
  if (!context.pendingConfirmPostId) return null;
  return context.items.find(
    (post) => post.postId === context.pendingConfirmPostId,
  ) ?? null;
}

function patchPost(
  items: readonly PrivateZoneVideoPost[],
  response: PrivateZonePostUnlockResponse,
): readonly PrivateZoneVideoPost[] {
  if (!response.post) return items;
  return items.map((post) =>
    post.postId === response.postId ? response.post! : post,
  );
}

function appendPosts(
  current: readonly PrivateZoneVideoPost[],
  response: PrivateZonePostsResponse,
): readonly PrivateZoneVideoPost[] {
  const byId = new Map(current.map((post) => [post.postId, post]));
  response.items.forEach((post) => byId.set(post.postId, post));
  return [...byId.values()];
}

const machineSetup = setup({
  types: {
    context: {} as PrivateZonePostState,
    events: {} as PrivateZonePostEvent,
    input: {} as { characterId: string },
  },
  actors: {
    loadPosts: loadPrivateZonePostsActor,
    unlockPost: unlockPrivateZonePostActor,
  },
  guards: {
    hasMore: ({ context }) => context.hasMore && !!context.nextCursor,
    hasPendingPost: ({ context }) => pendingPost(context) !== null,
  },
});

export const privateZonePostMachine = machineSetup.createMachine({
  id: "privateZonePosts",
  context: ({ input }) => createInitialPrivateZonePostState(input.characterId),
  initial: "idle",
  states: {
    idle: {
      on: { PrivateZonePostInit: "loading" },
    },
    loading: {
      entry: assign({
        errorMessage: null,
        unlockPaywallRequest: null,
      }),
      invoke: {
        src: "loadPosts",
        input: ({ context }) => ({ characterId: context.characterId }),
        onDone: {
          target: "ready",
          actions: assign(({ event }) => ({
            items: event.output.items,
            creditBalance: event.output.creditBalance,
            nextCursor: event.output.nextCursor,
            hasMore: event.output.hasMore,
            errorMessage: null,
          })),
        },
        onError: {
          target: "failed",
          actions: assign(({ event }) => ({
            errorMessage: errorMessage(event.error),
          })),
        },
      },
    },
    failed: {
      on: {
        PrivateZonePostInit: "loading",
        PrivateZonePostRefresh: "loading",
      },
    },
    ready: {
      on: {
        PrivateZonePostRefresh: "loading",
        PrivateZonePostLoadMore: {
          guard: "hasMore",
          target: "loadingMore",
        },
        PrivateZonePostUnlockRequested: {
          actions: assign(({ event }) => ({
            pendingConfirmPostId: event.postId,
            unlockErrorMessage: null,
            unlockPaywallRequest: null,
          })),
        },
        PrivateZonePostUnlockCancelled: {
          actions: assign({ pendingConfirmPostId: null }),
        },
        PrivateZonePostUnlockConfirmed: {
          guard: "hasPendingPost",
          target: "unlocking",
        },
        PrivateZonePostUnlockPaywallConsumed: {
          actions: assign({ unlockPaywallRequest: null }),
        },
      },
    },
    loadingMore: {
      invoke: {
        src: "loadPosts",
        input: ({ context }) => ({
          characterId: context.characterId,
          cursor: context.nextCursor,
        }),
        onDone: {
          target: "ready",
          actions: assign(({ context, event }) => ({
            items: appendPosts(context.items, event.output),
            creditBalance: event.output.creditBalance,
            nextCursor: event.output.nextCursor,
            hasMore: event.output.hasMore,
            errorMessage: null,
          })),
        },
        onError: {
          target: "ready",
          actions: assign(({ event }) => ({
            errorMessage: errorMessage(event.error),
          })),
        },
      },
    },
    unlocking: {
      entry: assign(({ context }) => ({
        unlockingPostId: context.pendingConfirmPostId,
        unlockErrorMessage: null,
        unlockPaywallRequest: null,
      })),
      invoke: {
        src: "unlockPost",
        input: ({ context }) => {
          const post = pendingPost(context);
          return {
            postId: post?.postId ?? "",
            expectedCost: post?.unlockCostCredits ?? 0,
          };
        },
        onDone: [
          {
            guard: ({ event }) => event.output.unlocked,
            target: "ready",
            actions: assign(({ context, event }) => ({
              items: patchPost(context.items, event.output),
              creditBalance: event.output.creditBalance,
              pendingConfirmPostId: null,
              unlockingPostId: null,
              unlockErrorMessage: null,
              unlockSuccessNonce: context.unlockSuccessNonce + 1,
            })),
          },
          {
            guard: ({ event }) =>
              event.output.reason === "insufficient_credits",
            target: "ready",
            actions: assign(({ context, event }) => ({
              items: patchPost(context.items, event.output),
              creditBalance: event.output.creditBalance,
              pendingConfirmPostId: null,
              unlockingPostId: null,
              unlockErrorMessage: null,
              unlockPaywallRequest: {
                postId: event.output.postId,
                reason: event.output.reason,
                requiredCredits:
                  event.output.requiredCredits ||
                  event.output.post?.unlockCostCredits ||
                  0,
                currentCredits:
                  event.output.currentCredits || event.output.creditBalance,
                shortfallCredits: event.output.shortfallCredits,
                ...(event.output.paymentGuidance
                  ? { paymentGuidance: event.output.paymentGuidance }
                  : {}),
              },
            })),
          },
          {
            guard: ({ event }) => event.output.reason === "cost_changed",
            target: "ready",
            actions: assign(({ context, event }) => ({
              items: patchPost(context.items, event.output),
              creditBalance: event.output.creditBalance,
              pendingConfirmPostId: null,
              unlockingPostId: null,
              unlockErrorMessage:
                "Unlock price changed. Please review the new price and confirm again.",
            })),
          },
          {
            guard: ({ event }) =>
              event.output.reason === "not_found" ||
              event.output.reason === "unavailable",
            target: "ready",
            actions: assign(({ context, event }) => ({
              items: context.items.filter(
                (post) => post.postId !== event.output.postId,
              ),
              pendingConfirmPostId: null,
              unlockingPostId: null,
              unlockErrorMessage: "This moment is no longer available.",
            })),
          },
          {
            target: "ready",
            actions: assign(({ context, event }) => ({
              items: patchPost(context.items, event.output),
              creditBalance: event.output.creditBalance,
              pendingConfirmPostId: null,
              unlockingPostId: null,
              unlockErrorMessage: "Unlock failed. Please try again.",
            })),
          },
        ],
        onError: {
          target: "ready",
          actions: assign(({ event }) => ({
            pendingConfirmPostId: null,
            unlockingPostId: null,
            unlockErrorMessage: errorMessage(event.error),
          })),
        },
      },
    },
  },
});

export type PrivateZonePostMachine = typeof privateZonePostMachine;
