<script setup lang="ts">
import {ref} from "vue";
import {
  CompositeDidDocumentResolver,
  CompositeHandleResolver,
  DohJsonHandleResolver,
  PlcDidDocumentResolver,
  WebDidDocumentResolver,
  WellKnownHandleResolver,
} from "@atcute/identity-resolver";
import {AtpAgent} from "@atproto/api";
import * as TID from "@atcute/tid";

//Should be using the lexicons to generate types...
// Types for fm.teal.alpha.feed.play
interface PlayArtist {
  artistMbId?: string;
  artistName: string;
}
interface PlayValue {
  $type?: "fm.teal.alpha.feed.play";
  artists?: PlayArtist[];
  // legacy support: earlier records may use artistNames
  artistNames?: string[];
  trackName: string;
  playedTime: string; // ISO string
  releaseMbId?: string;
  releaseName?: string;
  recordingMbId?: string;
  submissionClientAgent?: string;
}
interface ListRecord<T> {
  uri: string;
  cid: string;
  value: T;
}

// handle resolution
const handleResolver = new CompositeHandleResolver({
  strategy: "race",
  methods: {
    dns: new DohJsonHandleResolver({
      dohUrl: "https://mozilla.cloudflare-dns.com/dns-query",
    }),
    http: new WellKnownHandleResolver(),
  },
});

const docResolver = new CompositeDidDocumentResolver({
  methods: {
    plc: new PlcDidDocumentResolver(),
    web: new WebDidDocumentResolver(),
  },
});

const userHandle = ref("");
const loading = ref(false);
const artists = ref<{ name: string; plays: number }[]>([]);
const tracks = ref<{ name: string; artist: string; plays: number }[]>([]);
const topDays = ref<{ date: string; plays: number }[]>([]);
const totalSongs = ref(0);
const errorMessage = ref<string | null>(null);

// Advanced controls
const topLimit = ref<number>(25);
const startDate = ref<string | null>(null); // format: YYYY-MM-DD (local)
const endDate = ref<string | null>(null);

const formatNumber = (n: number) => n.toLocaleString();

// Returns YYYY-MM-DD in the browser's local time zone
const localDayKey = (iso?: string): string | null => {
  if (!iso) return null;
  const d = new Date(iso);
  if (isNaN(d.getTime())) return null;
  const y = d.getFullYear();
  const m = (d.getMonth() + 1).toString().padStart(2, "0");
  const day = d.getDate().toString().padStart(2, "0");
  return `${y}-${m}-${day}`;
};

const lookup = async () => {
  loading.value = true;
  //Clear values
  errorMessage.value = null;
  artists.value = [];
  tracks.value = [];
  topDays.value = [];
  totalSongs.value = 0;

  try {
    const did = await handleResolver.resolve(
        userHandle.value as `${string}.${string}`,
    );

    if (did == undefined) {
      throw new Error("expected handle to resolve");
    }
    console.log(did); // did:plc:ewvi7nxzyoun6zhxrhs64oiz

    const doc = await docResolver.resolve(did);
    console.log(doc);

    const endpoint = doc.service?.[0]?.serviceEndpoint as string | undefined;
    if (!endpoint) {
      throw new Error("could not resolve service endpoint for DID");
    }
    const agent = new AtpAgent({
      service: endpoint,
    });
    let cursor: string | null = null;
    let totalCount = 0;
    let inner_tracks: { name: string; artist: string; plays: number }[] = [];
    let inner_artists: { name: string; plays: number }[] = [];
    const dayCountMap = new Map<string, number>();

    //If dates are set want to reverse
    const reverse: boolean = startDate.value !== null && endDate.value !== null;
    if (reverse) {
      //Couple checks on dates
      if(endDate.value === null) {
        endDate.value = new Date().toISOString().split('T')[0];
      }

      if (startDate.value !== null && startDate.value > endDate.value) {
        throw new Error("Start date must be before end date");
      }

      //Get the tid of start time to filter in reverse since
      const startDateTime = new Date(startDate.value + 'T00:00:00.000');
      const micros = startDateTime.getTime() * 1000; // convert ms to µs
      //Might as well use a lucky number for clock id
      cursor = TID.create(micros, 23);
    }

    let response = await agent.com.atproto.repo.listRecords({
      repo: did,
      collection: "fm.teal.alpha.feed.play",
      limit: 100,
      reverse: reverse,
      ...(cursor ? { cursor } : {}),
    });


    // Process pages incrementally while fetching them
    let shouldStop = false;
    let endDateTime = new Date(endDate.value + 'T23:59:59.999');
    while (true) {
      // break
      const records = (response.data.records as unknown as ListRecord<PlayValue>[]) ?? [];
      totalCount += records.length;

      for (const play of records) {
        // spot-check if play is valid
        if (!play.value || !play.value.trackName) {
          continue;
        }

        if(endDateTime <= new Date(play.value.playedTime) ) {
          console.log("End of the line");
          shouldStop = true;
          break;
        }


        // Aggregate by artist(s)
        if (play.value?.artists) {
          for (const artist of play.value?.artists) {
            let alreadyPlayed = inner_artists.find(
                (a) => a.name === artist.artistName,
            );
            if (!alreadyPlayed) {
              inner_artists.push({
                name: artist.artistName,
                plays: 1,
              });
            } else {
              alreadyPlayed.plays++;
            }
          }
        } else if (play.value?.artistNames) {
          // old version of lexicon
          for (const arist of play.value?.artistNames) {
            let alreadyPlayed = inner_artists.find(
                (a) => a.name === arist,
            );
            if (!alreadyPlayed) {
              inner_artists.push({name: arist, plays: 1});
            } else {
              alreadyPlayed.plays++;
            }
          }
        }

        // Aggregate by track
        let alreadyPlayed = inner_tracks.find(
            (a) => a.name === play.value.trackName,
        );
        if (!alreadyPlayed && play?.value) {
          const primaryArtist = play.value.artists?.[0]?.artistName ?? play.value.artistNames?.[0] ?? "Unknown Artist";
          inner_tracks.push({
            name: play.value.trackName,
            artist: primaryArtist,
            plays: 1,
          });
        } else if (alreadyPlayed) {
          alreadyPlayed.plays++;
        }

        // Aggregate by local day using playedTime
        const key = localDayKey(play.value?.playedTime);
        if (key) {
          dayCountMap.set(key, (dayCountMap.get(key) ?? 0) + 1);
        }
      }

      // update reactive values incrementally (top N)
      artists.value = inner_artists
          .sort((a, b) => b.plays - a.plays)
          .slice(0, topLimit.value);
      tracks.value = inner_tracks
          .sort((a, b) => b.plays - a.plays)
          .slice(0, topLimit.value);
      totalSongs.value = totalCount;

      // compute top N days
      topDays.value = Array.from(dayCountMap.entries())
          .map(([date, plays]) => ({date, plays}))
          .sort((a, b) => b.plays - a.plays)
          .slice(0, topLimit.value);

      // update cursor and continue unless we've passed the start boundary
      cursor = response.data.cursor ?? null;
      if(shouldStop || cursor === null) {
        break;
      }

      response = await agent.com.atproto.repo.listRecords({
        repo: did,
        collection: "fm.teal.alpha.feed.play",
        limit: 100,
        reverse: reverse,
        ...(cursor ? { cursor } : {}),
      });
    }
  } catch (error: unknown) {
    errorMessage.value = error instanceof Error ? error.message : String(error);
    throw error;
  } finally {
    loading.value = false;
  }
};
</script>

<template>
  <div class="container mx-auto p-4 text-center">
    <h1
        class="text-5xl font-bold mb-2 bg-gradient-to-r from-teal-400 to-teal-600 text-transparent bg-clip-text"
    >
      teal.fm wrapped
    </h1>
    <p class="text-sm text-gray-500 mb-8">
      Mostly not affiliated with teal.fm™
    </p>
    <form @submit.prevent="lookup">

      <div class="join w-full justify-center">
        <input
            v-model="userHandle"
            type="text"
            placeholder="alice.bsky.social"
            class="input input-bordered join-item w-1/2 max-w-xs"
        />
        <button
            type="submit"
            class="btn join-item bg-teal-500 hover:bg-teal-600 text-white"
        >
          That's a wrap
        </button>
      </div>

      <!-- Advanced menu -->
      <div class="mt-4">
        <div class="collapse bg-base-200">
          <input type="checkbox" />
          <div class="collapse-title text-md font-medium">
            Advanced options
          </div>
          <div class="collapse-content">
            <div class="flex flex-col md:flex-row gap-4 items-center justify-center">
              <label class="form-control w-full max-w-xs">
                <div class="label">
                  <span class="label-text">Top records to show</span>
                </div>
                <input type="number" min="1" max="100" v-model.number="topLimit" class="input input-bordered w-full max-w-xs" />
              </label>
              <label class="form-control w-full max-w-xs">
                <div class="label">
                  <span class="label-text">Start date</span>
                </div>
                <input type="date" v-model="startDate" class="input input-bordered w-full max-w-xs" />
              </label>
              <label class="form-control w-full max-w-xs">
                <div class="label">
                  <span class="label-text">End date</span>
                </div>
                <input type="date" v-model="endDate" class="input input-bordered w-full max-w-xs" />
              </label>
            </div>
            <p class="text-xs text-gray-500 mt-2">Date range start is start of day, end is end of day</p>
          </div>
        </div>
      </div>

      <span class="text-red-500" v-if="errorMessage">{{errorMessage}}</span>
    </form>
    <div class="w-full justify-center">
            <span
                v-if="loading"
                class="loading loading-dots loading-lg mt-8"
            ></span>
      <div v-if="tracks.length > 0" class="mt-8">
        <h2 class="text-2xl font-bold mb-4">
          Top Songs out of {{ formatNumber(totalSongs) }}
        </h2>
        <div class="overflow-x-auto">
          <table class="table w-full">
            <thead>
            <tr>
              <th>Rank</th>
              <th>Plays</th>
              <th>Song</th>
            </tr>
            </thead>
            <tbody>
            <tr v-for="(track, idx) in tracks" :key="track.name">
              <td>{{ idx + 1 }}.</td>
              <td>{{ formatNumber(track.plays) }}</td>
              <td>{{ track.name }} by {{ track.artist }}</td>
            </tr>
            </tbody>
          </table>
        </div>
      </div>
      <div v-if="artists.length > 0" class="mt-8">
        <h2 class="text-2xl font-bold mb-4">Top Artists</h2>
        <div class="overflow-x-auto">
          <table class="table w-full">
            <thead>
            <tr>
              <th>Rank</th>
              <th>Plays</th>
              <th>Artist</th>
            </tr>
            </thead>
            <tbody>
            <tr v-for="(artist, idx) in artists" :key="artist.name">
              <td>{{ idx + 1 }}.</td>
              <td>{{ formatNumber(artist.plays) }}</td>
              <td>{{ artist.name }}</td>
            </tr>
            </tbody>
          </table>
        </div>
      </div>
      <div v-if="topDays.length > 0" class="mt-8">
        <h2 class="text-2xl font-bold mb-4">Top Days (Most Songs Played)</h2>
        <div class="overflow-x-auto">
          <table class="table w-full">
            <thead>
            <tr>
              <th>Rank</th>
              <th>Plays</th>
              <th>Date (yyyy-mm-dd)</th>
            </tr>
            </thead>
            <tbody>
            <tr v-for="(day, idx) in topDays" :key="day.date">
              <td>{{ idx + 1 }}.</td>
              <td>{{ formatNumber(day.plays) }}</td>
              <td>{{ day.date }}</td>
            </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.container {
  min-height: 50vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
</style>
