import {
  LocalActorResolver,
  CompositeHandleResolver,
  DohJsonHandleResolver,
  WellKnownHandleResolver,
  CompositeDidDocumentResolver,
  PlcDidDocumentResolver,
  WebDidDocumentResolver,
} from "@atcute/identity-resolver";

// Initialize resolvers for Cloudflare Workers
const handleResolver = new CompositeHandleResolver({
  strategy: "race",
  methods: {
    dns: new DohJsonHandleResolver({
      dohUrl: "https://cloudflare-dns.com/dns-query",
    }),
    http: new WellKnownHandleResolver(),
  },
});

// env only reaches us inside fetch, so the resolver is built on the first
// request and kept after that
let actorResolver;
let resolverApiUrl;

const getActorResolver = (env) => {
  const apiUrl = env.PLC_DIRECTORY_URL || undefined;
  if (!actorResolver || resolverApiUrl !== apiUrl) {
    resolverApiUrl = apiUrl;
    actorResolver = new LocalActorResolver({
      handleResolver,
      didDocumentResolver: new CompositeDidDocumentResolver({
        methods: {
          plc: new PlcDidDocumentResolver(apiUrl ? { apiUrl } : {}),
          web: new WebDidDocumentResolver(),
        },
      }),
    });
  }
  return actorResolver;
};

export default {
  async fetch(request, env) {
    // Helper function to generate a color from a string
    const stringToColor = (str) => {
      let hash = 0;
      for (let i = 0; i < str.length; i++) {
        hash = str.charCodeAt(i) + ((hash << 5) - hash);
      }
      let color = "#";
      for (let i = 0; i < 3; i++) {
        const value = (hash >> (i * 8)) & 0xff;
        color += ("00" + value.toString(16)).substr(-2);
      }
      return color;
    };

    // Helper function to fetch Tangled profile from PDS
    const getTangledAvatarFromPDS = async (actor) => {
      try {
        // Resolve the identity
        const identity = await getActorResolver(env).resolve(actor);
        if (!identity) {
          console.log({
            level: "debug",
            message: "failed to resolve identity",
            actor: actor,
          });
          return null;
        }

        const did = identity.did;
        const pdsEndpoint = identity.pds.replace(/\/$/, ""); // Remove trailing slash

        if (!pdsEndpoint) {
          console.log({
            level: "debug",
            message: "no PDS endpoint found",
            actor: actor,
            did: did,
          });
          return null;
        }

        const profileUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=sh.tangled.actor.profile&rkey=self`;

        // Fetch the Tangled profile record from PDS
        const profileResponse = await fetch(profileUrl);

        if (!profileResponse.ok) {
          console.log({
            level: "debug",
            message: "no Tangled profile found on PDS",
            actor: actor,
            status: profileResponse.status,
          });
          return null;
        }

        const profileData = await profileResponse.json();
        const avatarBlob = profileData?.value?.avatar;

        if (!avatarBlob) {
          console.log({
            level: "debug",
            message: "Tangled profile has no avatar",
            actor: actor,
          });
          return null;
        }

        // Extract CID from blob reference object
        // The ref might be an object with $link property or a string
        let avatarCID;
        if (typeof avatarBlob.ref === "string") {
          avatarCID = avatarBlob.ref;
        } else if (avatarBlob.ref?.$link) {
          avatarCID = avatarBlob.ref.$link;
        } else if (typeof avatarBlob === "string") {
          avatarCID = avatarBlob;
        }

        if (!avatarCID || typeof avatarCID !== "string") {
          console.log({
            level: "warn",
            message: "could not extract valid CID from avatar blob",
            actor: actor,
            avatarBlob: avatarBlob,
            avatarBlobRef: avatarBlob.ref,
          });
          return null;
        }

        // Construct blob URL (pdsEndpoint already has trailing slash removed)
        const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${avatarCID}`;

        return blobUrl;
      } catch (e) {
        console.log({
          level: "warn",
          message: "error fetching Tangled avatar from PDS",
          actor: actor,
          error: e.message,
        });
        return null;
      }
    };

    const url = new URL(request.url);
    const { pathname, searchParams } = url;

    if (!pathname || pathname === "/") {
      return new Response(
        `This is Tangled's avatar service. It fetches your pretty avatar from your PDS, Bluesky, or generates a placeholder.
You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`,
      );
    }

    const allowedWidths = [26, 34, 42, 52];
    const requestedSize = searchParams.get("size");

    let width = null;
    if (requestedSize === "tiny") {
      width = 32;
    } else if (allowedWidths.includes(Number(requestedSize))) {
      width = Number(requestedSize);
    }
    const format = searchParams.get("format") || "webp";
    const validFormats = ["webp", "jpeg", "png"];
    const outputFormat = validFormats.includes(format) ? format : "webp";
    
    const contentTypes = {
      webp: "image/webp",
      jpeg: "image/jpeg",
      png: "image/png",
    };

    const cache = caches.default;
    let cacheKey = request.url;
    let response = await cache.match(cacheKey);
    if (response) return response;

    const pathParts = pathname.slice(1).split("/");
    if (pathParts.length < 2) {
      return new Response("Bad URL", { status: 400 });
    }

    const [signatureHex, actor] = pathParts;
    const actorBytes = new TextEncoder().encode(actor);

    const key = await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(env.AVATAR_SHARED_SECRET),
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign", "verify"],
    );

    const computedSigBuffer = await crypto.subtle.sign("HMAC", key, actorBytes);
    const computedSig = Array.from(new Uint8Array(computedSigBuffer))
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");

    console.log({
      level: "debug",
      message: "avatar request for: " + actor,
      computedSignature: computedSig,
      providedSignature: signatureHex,
    });

    const sigBytes = Uint8Array.from(
      signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)),
    );
    const valid = await crypto.subtle.verify("HMAC", key, sigBytes, actorBytes);

    if (!valid) {
      return new Response("Invalid signature", { status: 403 });
    }

    try {
      let avatarUrl = null;

      // Try to get Tangled avatar from user's PDS first
      avatarUrl = await getTangledAvatarFromPDS(actor);

      // If no Tangled avatar, fall back to Bluesky
      if (!avatarUrl) {
        console.log({
          level: "debug",
          message: "no Tangled avatar, falling back to Bluesky",
          actor: actor,
        });

        const profileResponse = await fetch(
          `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
        );

        if (profileResponse.ok) {
          const profile = await profileResponse.json();
          avatarUrl = profile.avatar;
        }
      }

      if (!avatarUrl) {
        // Generate a random color based on the actor string
        console.log({
          level: "debug",
          message: "no avatar found, generating placeholder",
          actor: actor,
        });

        const bgColor = stringToColor(actor);
        const size = width ?? 128;
        const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg"><rect width="${size}" height="${size}" fill="${bgColor}"/></svg>`;
        const svgData = new TextEncoder().encode(svg);

        response = new Response(svgData, {
          headers: {
            "Content-Type": "image/svg+xml",
            "Cache-Control": "public, max-age=43200",
          },
        });
        await cache.put(cacheKey, response.clone());
        return response;
      }

      // Fetch and optionally resize the avatar
      let avatarResponse;
      const cfOptions = outputFormat !== "webp" || width ? {
        cf: {
          image: {
            format: outputFormat,
            ...(width ? { width, height: width, fit: "cover" } : {}),
          },
        },
      }: {};

      avatarResponse = await fetch(avatarUrl, cfOptions);

      if (!avatarResponse.ok) {
        return new Response(`failed to fetch avatar for ${actor}.`, {
          status: avatarResponse.status,
        });
      }

      const avatarData = await avatarResponse.arrayBuffer();

      response = new Response(avatarData, {
        headers: {
          "Content-Type": contentTypes[outputFormat],
          "Cache-Control": "public, max-age=43200",
        },
      });

      await cache.put(cacheKey, response.clone());
      return response;
    } catch (error) {
      return new Response(`error fetching avatar: ${error.message}`, {
        status: 500,
      });
    }
  },
};
