import {
  createSignal,
  For,
  Switch,
  Match,
  Show,
  type Component,
} from "solid-js";
import { createStore } from "solid-js/store";

import { Agent } from "@atproto/api";
import { BrowserOAuthClient } from "@atproto/oauth-client-browser";

enum RepoStatus {
  BLOCKEDBY = 1 << 0,
  BLOCKING = 1 << 1,
  DELETED = 1 << 2,
  DEACTIVATED = 1 << 3,
  SUSPENDED = 1 << 4,
  YOURSELF = 1 << 5,
  NONMUTUAL = 1 << 6,
}

type FollowRecord = {
  did: string;
  handle: string;
  uri: string;
  status: RepoStatus;
  toBeDeleted: boolean;
  visible: boolean;
};

const [followRecords, setFollowRecords] = createStore<FollowRecord[]>([]);
const [loginState, setLoginState] = createSignal<boolean>();
const [notice, setNotice] = createSignal("");

const client = await BrowserOAuthClient.load({
  clientId: "https://cleanfollow-bsky.pages.dev/client-metadata.json",
  handleResolver: "https://boletus.us-west.host.bsky.network",
});

client.addEventListener("deleted", () => {
  setLoginState(false);
});

let agent: Agent;
let userHandle: string;

const result = await client.init().catch(() => {});

if (result) {
  agent = new Agent(result.session);
  setLoginState(true);
  const res = await agent.getProfile({ actor: agent.did! });
  userHandle = res.data.handle;
}

const loginBsky = async (handle: string) => {
  setNotice("Redirecting...");
  try {
    await client.signIn(handle, {
      scope: "atproto transition:generic",
      signal: new AbortController().signal,
    });
  } catch (err) {
    setNotice("Error during OAuth redirection");
  }
};

const logoutBsky = async () => {
  if (result) await client.revoke(result.session.sub);
};

const Follows: Component = () => {
  function selectRecords(status: RepoStatus, toBeDeleted: boolean) {
    followRecords.forEach((record, index) => {
      if (record.status & status)
        setFollowRecords(index, "toBeDeleted", toBeDeleted);
    });
  }

  function changeVisibility(status: RepoStatus, visible: boolean) {
    followRecords.forEach((record, index) => {
      if (record.status & status) setFollowRecords(index, "visible", visible);
    });
  }

  const options: { status: RepoStatus; label: string }[] = [
    { status: RepoStatus.DELETED, label: "Deleted" },
    { status: RepoStatus.DEACTIVATED, label: "Deactivated" },
    { status: RepoStatus.SUSPENDED, label: "Suspended" },
    { status: RepoStatus.BLOCKEDBY, label: "Blocked By" },
    { status: RepoStatus.BLOCKING, label: "Blocking" },
    { status: RepoStatus.NONMUTUAL, label: "Non Mutual" },
  ];

  return (
    <div class="mt-3 flex flex-col sm:flex-row">
      <div class="mr-5 mb-3 pb-3 justify-around sm:mb-0 sm:top-3 top-0 border-b border-b-gray-400 sm:border-none w-full bg-white sm:w-auto sticky sm:self-start flex flex-wrap sm:flex-col">
        <For each={options}>
          {(option, index) => (
            <div
              classList={{
                "sm:pb-2 min-w-36 sm:mb-2 mt-3 sm:mt-0": true,
                "sm:border-b sm:border-b-gray-300":
                  index() < options.length - 1,
              }}
            >
              <div>
                <label class="inline-flex items-center mt-1 mb-2 cursor-pointer">
                  <input
                    type="checkbox"
                    class="sr-only peer"
                    checked
                    onChange={(e) =>
                      changeVisibility(option.status, e.currentTarget.checked)
                    }
                  />
                  <span class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></span>
                  <span class="ms-3 dark:text-gray-300 select-none">
                    {option.label}
                  </span>
                </label>
              </div>
              <div class="flex items-center">
                <input
                  type="checkbox"
                  id={option.label}
                  class="h-4 w-4 rounded"
                  onChange={(e) =>
                    selectRecords(option.status, e.currentTarget.checked)
                  }
                />
                <label for={option.label} class="ml-2 select-none">
                  Select All
                </label>
              </div>
            </div>
          )}
        </For>
      </div>
      <div>
        <For each={followRecords}>
          {(record, index) => (
            <Show when={record.visible}>
              <div class="flex items-center border-b mb-2 pb-2">
                <div class="mr-4">
                  <input
                    type="checkbox"
                    id={"record" + index()}
                    class="h-4 w-4 rounded border-gray-400 text-indigo-600 focus:ring-indigo-600"
                    checked={record.toBeDeleted}
                    onChange={(e) =>
                      setFollowRecords(
                        index(),
                        "toBeDeleted",
                        e.currentTarget.checked,
                      )
                    }
                  />
                </div>
                <div>
                  <label for={"record" + index()} class="flex flex-col">
                    <span>@{record.handle}</span>
                    <span>{record.did}</span>
                    <span>
                      <Switch>
                        <Match
                          when={
                            record.status ==
                            (RepoStatus.BLOCKEDBY | RepoStatus.BLOCKING)
                          }
                        >
                          Mutual Block
                        </Match>
                        <Match when={record.status == RepoStatus.DELETED}>
                          Deleted
                        </Match>
                        <Match when={record.status == RepoStatus.DEACTIVATED}>
                          Deactivated
                        </Match>
                        <Match when={record.status == RepoStatus.BLOCKEDBY}>
                          Blocked by
                        </Match>
                        <Match when={record.status == RepoStatus.BLOCKING}>
                          Blocking
                        </Match>
                        <Match when={record.status == RepoStatus.SUSPENDED}>
                          Suspended
                        </Match>
                        <Match when={record.status == RepoStatus.YOURSELF}>
                          Literally Yourself
                        </Match>
                        <Match when={record.status == RepoStatus.NONMUTUAL}>
                          Non Mutual
                        </Match>
                      </Switch>
                    </span>
                  </label>
                </div>
              </div>
            </Show>
          )}
        </For>
      </div>
    </div>
  );
};

const Form: Component = () => {
  const [loginInput, setLoginInput] = createSignal("");
  const [progress, setProgress] = createSignal(0);
  const [followCount, setFollowCount] = createSignal(0);

  const fetchHiddenAccounts = async () => {
    const fetchFollows = async () => {
      const PAGE_LIMIT = 100;
      const fetchPage = async (cursor?: any) => {
        return await agent.com.atproto.repo.listRecords({
          repo: agent.did!,
          collection: "app.bsky.graph.follow",
          limit: PAGE_LIMIT,
          cursor: cursor,
        });
      };

      let res = await fetchPage();
      let follows = res.data.records;

      while (res.data.cursor && res.data.records.length >= PAGE_LIMIT) {
        res = await fetchPage(res.data.cursor);
        follows = follows.concat(res.data.records);
      }

      return follows;
    };

    setNotice("");
    setProgress(0);

    await fetchFollows().then((follows) =>
      follows.forEach(async (record: any) => {
        setFollowCount(follows.length);

        try {
          const res = await agent.getProfile({
            actor: record.value.subject,
          });

          const viewer = res.data.viewer!;
          let status: RepoStatus | undefined = undefined;

          if (!viewer.followedBy) status = RepoStatus.NONMUTUAL;

          if (viewer.blockedBy) {
            status =
              viewer.blocking || viewer.blockingByList
                ? RepoStatus.BLOCKEDBY | RepoStatus.BLOCKING
                : RepoStatus.BLOCKEDBY;
          } else if (res.data.did.includes(agent.did!)) {
            status = RepoStatus.YOURSELF;
          } else if (viewer.blocking || viewer.blockingByList) {
            status = RepoStatus.BLOCKING;
          }

          if (status !== undefined) {
            setFollowRecords(followRecords.length, {
              did: record.value.subject,
              handle: res.data.handle,
              uri: record.uri,
              status: status,
              toBeDeleted: false,
              visible: true,
            });
          }
        } catch (e: any) {
          const res = await fetch(
            record.value.subject.startsWith("did:web")
              ? "https://" +
                  record.value.subject.split(":")[2] +
                  "/.well-known/did.json"
              : "https://plc.directory/" + record.value.subject,
          );

          const status = e.message.includes("not found")
            ? RepoStatus.DELETED
            : e.message.includes("deactivated")
              ? RepoStatus.DEACTIVATED
              : e.message.includes("suspended")
                ? RepoStatus.SUSPENDED
                : undefined;

          const handle = await res.json().then((doc) => {
            for (const alias of doc.alsoKnownAs) {
              if (alias.includes("at://")) {
                return alias.split("//")[1];
              }
            }
          });

          if (status !== undefined) {
            setFollowRecords(followRecords.length, {
              did: record.value.subject,
              handle: handle,
              uri: record.uri,
              status: status,
              toBeDeleted: false,
              visible: true,
            });
          }
        }
        setProgress(progress() + 1);
      }),
    );
  };

  const unfollow = async () => {
    const writes = followRecords
      .filter((record) => record.toBeDeleted)
      .map((record) => {
        return {
          $type: "com.atproto.repo.applyWrites#delete",
          collection: "app.bsky.graph.follow",
          rkey: record.uri.split("/").pop(),
        };
      });

    const BATCHSIZE = 200;
    for (let i = 0; i < writes.length; i += BATCHSIZE) {
      await agent.com.atproto.repo.applyWrites({
        repo: agent.did!,
        writes: writes.slice(i, i + BATCHSIZE),
      });
    }

    setFollowRecords([]);
    setProgress(0);
    setFollowCount(0);
    setNotice(
      `Unfollowed ${writes.length} account${writes.length > 1 ? "s" : ""}`,
    );
  };

  return (
    <div class="flex flex-col items-center">
      <Show when={!loginState()}>
        <form
          class="flex flex-col items-center"
          onsubmit={(e) => e.preventDefault()}
        >
          <label for="handle">Handle:</label>
          <input
            type="text"
            id="handle"
            placeholder="user.bsky.social"
            class="rounded-md mt-1 py-1 px-2 mb-3"
            onInput={(e) => setLoginInput(e.currentTarget.value)}
          />
          <button
            onclick={() => loginBsky(loginInput())}
            class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
          >
            Login
          </button>
        </form>
      </Show>
      <Show when={loginState()}>
        <div class="mb-5">
          Logged in as {userHandle} (
          <a href="" class="text-red-600" onclick={() => logoutBsky()}>
            Logout
          </a>
          )
        </div>
        <Show when={!followRecords.length}>
          <button
            type="button"
            onclick={() => fetchHiddenAccounts()}
            class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
          >
            Preview
          </button>
        </Show>
        <Show when={followRecords.length}>
          <button
            type="button"
            onclick={() => unfollow()}
            class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
          >
            Confirm
          </button>
        </Show>
      </Show>
      <Show when={notice()}>
        <div class="m-3">{notice()}</div>
      </Show>
      <Show when={loginState() && followCount()}>
        <div class="m-3">
          Progress: {progress()}/{followCount()}
        </div>
      </Show>
    </div>
  );
};

const App: Component = () => {
  return (
    <div class="flex flex-col items-center m-5">
      <h1 class="text-2xl mb-5">cleanfollow-bsky</h1>
      <div class="mb-3 text-center">
        <p>Unfollow blocked, deleted, suspended, and deactivated accounts</p>
        <p>By default, every account will be unselected</p>
        <div>
          <a
            class="text-blue-600 hover:underline"
            href="https://github.com/notjuliet/cleanfollow-bsky"
          >
            Source Code
          </a>
          <span> | </span>
          <a
            class="text-blue-600 hover:underline"
            href="https://bsky.app/profile/adorable.mom"
          >
            Bluesky
          </a>
          <span> | </span>
          <a
            class="text-blue-600 hover:underline"
            href="https://mary-ext.codeberg.page/bluesky-quiet-posters/"
          >
            Quiet Posters
          </a>
        </div>
      </div>
      <Form />
      <Show when={loginState() && followRecords.length}>
        <Follows />
      </Show>
    </div>
  );
};

export default App;
