import { getAdapter }        from '$lib/request-manager'
import { settingsState }     from '$lib/state/settings.svelte'
import { buildChapterList, type ChapterDisplayPrefs } from '$lib/components/series/lib/chapterList'
import { syncBackFromTracker }                        from '$lib/components/tracking/lib/trackingSync'
import type { Tracker, TrackRecord }  from '$lib/types'
import type { Chapter }               from '$lib/types'
import type { TrackerWithRecords }    from '$lib/components/tracking/lib/trackingSync'

const BOOT_SYNC_RATE_MS = 400

type RecordMap   = Map<number, TrackRecord[]>
type MangaBucket = { mangaId: number; records: TrackRecord[] }

class TrackingState {
  private byManga: RecordMap = $state(new Map())

  allTrackers: TrackerWithRecords[] = $state([])
  loadingAll:  boolean              = $state(false)
  loadingFor:  Set<number>          = $state(new Set())
  error:       string | null        = $state(null)

  trackers:       Tracker[]     = $state([])
  loading:        boolean       = $state(false)
  syncing:        boolean       = $state(false)
  recordsLoading: boolean       = $state(false)
  recordsError:   string | null = $state(null)
  searchResults:  unknown[]     = $state([])
  searchLoading:  boolean       = $state(false)
  searchError:    string | null = $state(null)

  recordsFor(mangaId: number): TrackRecord[] {
    return this.byManga.get(mangaId) ?? []
  }

  private setFor(mangaId: number, records: TrackRecord[]) {
    const next = new Map(this.byManga)
    next.set(mangaId, records)
    this.byManga = next
  }

  private patchFor(mangaId: number, updated: Partial<TrackRecord> & { id: number }) {
    const records = this.recordsFor(mangaId).map((r) =>
      r.id === updated.id ? { ...r, ...updated } : r
    )
    this.setFor(mangaId, records)

    this.allTrackers = this.allTrackers.map((t) => ({
      ...t,
      trackRecords: {
        nodes: t.trackRecords.nodes.map((r) =>
          r.id === updated.id ? { ...r, ...updated } : r
        ),
      },
    }))
  }


  async loadForManga(mangaId: number) {
    if (this.loadingFor.has(mangaId)) return
    const existing = this.byManga.get(mangaId)
    if (existing && existing.length > 0) return

    const next = new Set(this.loadingFor)
    next.add(mangaId)
    this.loadingFor = next

    try {
      const records = await getAdapter().getMangaTrackRecords(String(mangaId)) as TrackRecord[]
      this.setFor(mangaId, records)
    } catch (e: unknown) {
      this.error = e instanceof Error ? e.message : 'Failed to load tracking'
    } finally {
      const s = new Set(this.loadingFor)
      s.delete(mangaId)
      this.loadingFor = s
    }
  }

  async loadAll() {
    this.loadingAll = true
    this.error      = null
    try {
      const trackers = await getAdapter().getAllTrackerRecords() as TrackerWithRecords[]
      this.allTrackers = trackers
      this.trackers    = trackers

      for (const tracker of trackers.filter((t) => t.isLoggedIn)) {
        for (const record of tracker.trackRecords.nodes) {
          if (!record.manga?.id) continue
          const mangaId  = record.manga.id
          const existing = this.byManga.get(mangaId) ?? []
          const merged   = [...existing.filter((r) => r.id !== record.id), record]
          this.setFor(mangaId, merged)
        }
      }
    } catch (e: unknown) {
      this.error = e instanceof Error ? e.message : 'Failed to load tracking'
    } finally {
      this.loadingAll = false
    }
  }

  async updateStatus(mangaId: number, record: TrackRecord, status: number): Promise<TrackRecord> {
    const fresh = await getAdapter().updateTrackRecord(String(record.id), { status })
    this.patchFor(mangaId, fresh)
    return fresh
  }

  async updateScore(mangaId: number, record: TrackRecord, scoreString: string): Promise<TrackRecord> {
    const score = parseFloat(scoreString)
    const fresh = await getAdapter().updateTrackRecord(String(record.id), { score: isNaN(score) ? undefined : score })
    this.patchFor(mangaId, fresh)
    return fresh
  }

  async updateChapterProgress(mangaId: number, record: TrackRecord, lastChapterRead: number): Promise<TrackRecord> {
    const fresh = await getAdapter().updateTrackRecord(String(record.id), { lastChapterRead })
    this.patchFor(mangaId, fresh)
    return fresh
  }

  async unbind(mangaId: number, record: TrackRecord) {
    await getAdapter().unlinkTracker(String(record.id))
    this.setFor(mangaId, this.recordsFor(mangaId).filter((r) => r.id !== record.id))
    this.allTrackers = this.allTrackers.map((t) => ({
      ...t,
      trackRecords: { nodes: t.trackRecords.nodes.filter((r) => r.id !== record.id) },
    }))
  }


  async syncFromRemote(
    mangaId:  number,
    record:   TrackRecord,
    chapters: Chapter[],
    prefs:    ChapterDisplayPrefs,
  ): Promise<{ fresh: TrackRecord; markedIds: number[] }> {
    const fresh = await getAdapter().fetchTrackRecord(String(record.id)) as TrackRecord
    this.patchFor(mangaId, fresh)

    const markedIds = await this._applyRemoteProgress(fresh, chapters, prefs)
    return { fresh, markedIds }
  }

  private async _applyRemoteProgress(
    record:   TrackRecord,
    chapters: Chapter[],
    prefs:    ChapterDisplayPrefs,
  ): Promise<number[]> {
    if (!settingsState.settings.trackerSyncBack) return []

    return syncBackFromTracker(
      [record],
      chapters,
      {
        threshold:              settingsState.settings.trackerSyncBackThreshold ?? null,
        respectScanlatorFilter: settingsState.settings.trackerRespectScanlatorFilter ?? true,
        chapterPrefs:           prefs,
      },
      (ids, read) => getAdapter().markChaptersRead(ids, read),
    )
  }


  async updateFromRead(
    mangaId:     number,
    chapter:     Chapter,
    chapterList: Chapter[],
    prefs:       ChapterDisplayPrefs,
  ) {
    const filtered = buildChapterList(chapterList, { ...prefs, sortDir: 'asc' })
    const idx      = filtered.findIndex((c) => c.id === chapter.id)
    if (idx < 0) return
    const position = idx + 1

    const records = this.recordsFor(mangaId)
    for (const record of records) {
      try {
        const completedValue = this._completedStatusFor(record.trackerId)
        const isCompleted    = completedValue !== null && record.status === completedValue
        const readingValue   = this._readingStatusFor(record.trackerId)
        const belowMax       = record.totalChapters > 0 && (record.lastChapterRead ?? 0) < record.totalChapters

        if ((isCompleted || belowMax) && readingValue !== null && position > (record.lastChapterRead ?? 0)) {
          const fresh = await getAdapter().updateTrackRecord(String(record.id), {
            lastChapterRead: position,
            status:          readingValue,
          })
          this.patchFor(mangaId, fresh)
        } else if (!isCompleted && position > (record.lastChapterRead ?? 0)) {
          await this.updateChapterProgress(mangaId, record, position)
        }
      } catch {}
    }
  }

  async updateFromUnread(
    mangaId:     number,
    chapterList: Chapter[],
    prefs:       ChapterDisplayPrefs,
  ) {
    const filtered = buildChapterList(chapterList, { ...prefs, sortDir: 'asc' })
    const lastRead = [...filtered].reverse().find((c) => c.read)
    const position = lastRead ? filtered.findIndex((c) => c.id === lastRead.id) + 1 : 0

    const records = this.recordsFor(mangaId)
    for (const record of records.filter((r) => (r.lastChapterRead ?? 0) > position)) {
      try {
        const completedValue = this._completedStatusFor(record.trackerId)
        const isCompleted    = completedValue !== null && record.status === completedValue
        const belowMax       = record.totalChapters > 0 && position < record.totalChapters
        const readingValue   = this._readingStatusFor(record.trackerId)

        if ((isCompleted || belowMax) && readingValue !== null) {
          const fresh = await getAdapter().updateTrackRecord(String(record.id), {
            lastChapterRead: position,
            status:          readingValue,
          })
          this.patchFor(mangaId, fresh)
        } else {
          await this.updateChapterProgress(mangaId, record, position)
        }
      } catch {}
    }
  }


  async bootSync() {
    if (!settingsState.settings.trackerSyncBack) return
    if (this.allTrackers.length === 0) await this.loadAll()

    const buckets = new Map<number, MangaBucket>()

    for (const tracker of this.allTrackers.filter((t) => t.isLoggedIn)) {
      const completedValue = this._completedStatusFor(tracker.id)
      for (const record of tracker.trackRecords.nodes) {
        const mangaId = record.manga?.id
        if (!mangaId) continue
        if (completedValue !== null && record.status === completedValue) continue
        const bucket = buckets.get(mangaId) ?? { mangaId, records: [] }
        bucket.records.push(record)
        buckets.set(mangaId, bucket)
      }
    }

    const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))

    for (const { mangaId, records } of buckets.values()) {
      const prefs = { ...(settingsState.settings.mangaPrefs?.[mangaId] ?? {}) } as ChapterDisplayPrefs

      let chapters: Chapter[]
      try {
        chapters = await getAdapter().getChapters(String(mangaId))
      } catch {
        continue
      }

      const freshRecords: TrackRecord[] = []
      for (const record of records) {
        await delay(BOOT_SYNC_RATE_MS)
        try {
          const fresh = await getAdapter().fetchTrackRecord(String(record.id)) as TrackRecord
          this.patchFor(mangaId, fresh)
          freshRecords.push(fresh)
        } catch {
          freshRecords.push(record)
        }
      }

      try {
        await syncBackFromTracker(
          freshRecords,
          chapters,
          {
            threshold:              settingsState.settings.trackerSyncBackThreshold ?? null,
            respectScanlatorFilter: settingsState.settings.trackerRespectScanlatorFilter ?? true,
            chapterPrefs:           prefs,
          },
          (ids, read) => getAdapter().markChaptersRead(ids, read),
        )
      } catch {}
    }
  }

  clear(mangaId: number) {
    const next = new Map(this.byManga)
    next.delete(mangaId)
    this.byManga = next
  }


  private _statusesFor(trackerId: number): { value: number; name: string }[] {
    return this.allTrackers.find((t) => t.id === trackerId)?.statuses ?? []
  }

  private _completedStatusFor(trackerId: number): number | null {
    const s = this._statusesFor(trackerId).find((s) => s.name.toLowerCase() === 'completed')
    return s?.value ?? null
  }

  private _readingStatusFor(trackerId: number): number | null {
    const s = this._statusesFor(trackerId).find((s) => s.name.toLowerCase() === 'reading')
    return s?.value ?? null
  }
}

export const trackingState = new TrackingState()