import { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import JSZip from 'jszip'
import { ROUTE_PATHS } from '../app/route-paths'
import { parseSSC } from '../game/sscParser'
import type { ParsedChart } from '../game/sscParser'
import { useCustomSongsStore } from '../stores/custom-songs/store'
import type { CustomSongMeta } from '../stores/custom-songs/types'
import styles from './UploadPage.module.css'

const AUDIO_EXTS = ['.ogg', '.mp3', '.wav']
const CHART_EXTS = ['.ssc', '.sm']
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.bmp']

const PALETTE = [
  '#d946ef', '#e91e63', '#f44336', '#ff9800', '#4caf50',
  '#2196f3', '#9c27b0', '#00bcd4', '#ff5722', '#607d8b',
]

type SongBundle = {
  id: string
  chart: ParsedChart
  chartFile: File
  audioFile: File
  bgFile: File | null
  bannerFile: File | null
  selectedDifficulty: string
  simplify: boolean
}

function ext(name: string): string {
  const i = name.lastIndexOf('.')
  return i >= 0 ? name.slice(i).toLowerCase() : ''
}

function basename(path: string): string {
  const parts = path.split('/')
  return parts[parts.length - 1]
}

function generateId(): string {
  return `custom-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}

function estimateDuration(chart: ParsedChart): string {
  let maxTime = 0
  for (const d of chart.difficulties) {
    for (const n of d.notes) {
      if (n.time > maxTime) maxTime = n.time
    }
  }
  const mins = Math.floor(maxTime / 60)
  const secs = Math.floor(maxTime % 60)
  return `${mins}:${secs.toString().padStart(2, '0')}`
}

function easiestDifficulty(chart: ParsedChart): string {
  if (chart.difficulties.length === 0) return ''
  const beginner = chart.difficulties.find((d) => d.difficulty === 'Beginner')
  if (beginner) return beginner.difficulty
  return chart.difficulties.reduce((a, b) => (a.meter < b.meter ? a : b)).difficulty
}

/** Create a 1x1 transparent PNG as placeholder */
function placeholderImageFile(): File {
  const canvas = document.createElement('canvas')
  canvas.width = 1
  canvas.height = 1
  const ctx = canvas.getContext('2d')!
  ctx.fillStyle = 'rgba(0,0,0,0)'
  ctx.fillRect(0, 0, 1, 1)
  const dataUrl = canvas.toDataURL('image/png')
  const binary = atob(dataUrl.split(',')[1])
  const bytes = new Uint8Array(binary.length)
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
  return new File([bytes], 'placeholder.png', { type: 'image/png' })
}

async function parseBundleFromFiles(files: File[]): Promise<SongBundle[]> {
  const chartFiles = files.filter((f) => CHART_EXTS.includes(ext(f.name)))
  const audioFiles = files.filter((f) => AUDIO_EXTS.includes(ext(f.name)))
  const imageFiles = files.filter((f) => IMAGE_EXTS.includes(ext(f.name)))

  if (chartFiles.length === 0) throw new Error('No chart file (.ssc or .sm) found')
  if (audioFiles.length === 0) throw new Error('No audio file (.ogg, .mp3, .wav) found')

  const chartText = await chartFiles[0].text()
  const chart = parseSSC(chartText)

  if (chart.difficulties.length === 0) throw new Error('No playable difficulties found in chart')

  const bgFile = imageFiles.find((f) => /bg/i.test(f.name)) ?? imageFiles[0] ?? null
  const bannerFile = imageFiles.find((f) => /bn|banner/i.test(f.name)) ?? bgFile

  return [{
    id: generateId(),
    chart,
    chartFile: chartFiles[0],
    audioFile: audioFiles[0],
    bgFile,
    bannerFile,
    selectedDifficulty: easiestDifficulty(chart),
    simplify: true,
  }]
}

async function parseBundlesFromZip(zipFile: File): Promise<SongBundle[]> {
  const zip = await JSZip.loadAsync(zipFile)
  const entries = Object.entries(zip.files).filter(([, f]) => !f.dir)

  // Group files by parent directory
  const dirs = new Map<string, Map<string, JSZip.JSZipObject>>()
  for (const [path, entry] of entries) {
    const parts = path.split('/')
    const fileName = parts.pop()!
    const dir = parts.join('/')
    if (!dirs.has(dir)) dirs.set(dir, new Map())
    dirs.get(dir)!.set(fileName, entry)
  }

  // Find directories containing chart files
  const bundles: SongBundle[] = []
  for (const [, fileMap] of dirs) {
    let chartEntry: JSZip.JSZipObject | null = null
    let chartName = ''

    for (const [name, entry] of fileMap) {
      if (CHART_EXTS.includes(ext(name))) {
        chartEntry = entry
        chartName = name
        break
      }
    }

    if (!chartEntry) continue

    const chartText = await chartEntry.async('string')
    const chart = parseSSC(chartText)
    if (chart.difficulties.length === 0) continue

    // Find audio: use #MUSIC reference first, then any audio file
    let audioName = ''
    if (chart.musicFile) {
      const musicBase = basename(chart.musicFile)
      for (const name of fileMap.keys()) {
        if (name.toLowerCase() === musicBase.toLowerCase()) {
          audioName = name
          break
        }
      }
    }
    if (!audioName) {
      for (const name of fileMap.keys()) {
        if (AUDIO_EXTS.includes(ext(name))) { audioName = name; break }
      }
    }
    if (!audioName) continue // can't play without audio

    // Find images
    let bgName = ''
    let bnName = ''
    for (const name of fileMap.keys()) {
      if (!IMAGE_EXTS.includes(ext(name))) continue
      if (/bg|background/i.test(name)) bgName = name
      else if (/bn|banner/i.test(name)) bnName = name
      else if (!bgName) bgName = name
    }

    const toFile = async (name: string, entry: JSZip.JSZipObject) => {
      const blob = await entry.async('blob')
      return new File([blob], name, { type: blob.type || 'application/octet-stream' })
    }

    const audioFile = await toFile(audioName, fileMap.get(audioName)!)
    const chartFile = await toFile(chartName, chartEntry)
    const bgFile = bgName ? await toFile(bgName, fileMap.get(bgName)!) : null
    const bannerFile = bnName ? await toFile(bnName, fileMap.get(bnName)!) : bgFile

    bundles.push({
      id: generateId(),
      chart,
      chartFile,
      audioFile,
      bgFile,
      bannerFile,
      selectedDifficulty: easiestDifficulty(chart),
      simplify: true,
    })
  }

  if (bundles.length === 0) throw new Error('No valid song bundles found in zip')
  return bundles
}

export function UploadPage() {
  const navigate = useNavigate()
  const addSong = useCustomSongsStore((s) => s.addSong)

  const [bundles, setBundles] = useState<SongBundle[]>([])
  const [storeInBrowser, setStoreInBrowser] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [processing, setProcessing] = useState(false)
  const [dragOver, setDragOver] = useState(false)

  const fileInputRef = useRef<HTMLInputElement>(null)

  const handleFiles = useCallback(async (files: File[]) => {
    setError(null)
    setProcessing(true)
    try {
      const isZip = files.length === 1 && /\.zip$/i.test(files[0].name)
      const parsed = isZip
        ? await parseBundlesFromZip(files[0])
        : await parseBundleFromFiles(files)
      setBundles((prev) => [...prev, ...parsed])
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Failed to parse files')
    } finally {
      setProcessing(false)
    }
  }, [])

  const handleDrop = useCallback(
    (e: React.DragEvent) => {
      e.preventDefault()
      setDragOver(false)
      const files = Array.from(e.dataTransfer.files)
      if (files.length > 0) handleFiles(files)
    },
    [handleFiles],
  )

  const handleDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    setDragOver(true)
  }, [])

  const handleDragLeave = useCallback(() => setDragOver(false), [])

  const handleFileInput = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const files = Array.from(e.target.files ?? [])
      if (files.length > 0) handleFiles(files)
      e.target.value = ''
    },
    [handleFiles],
  )

  const updateBundle = useCallback((id: string, patch: Partial<SongBundle>) => {
    setBundles((prev) => prev.map((b) => (b.id === id ? { ...b, ...patch } : b)))
  }, [])

  const removeBundle = useCallback((id: string) => {
    setBundles((prev) => prev.filter((b) => b.id !== id))
  }, [])

  const handleAdd = useCallback(async () => {
    setProcessing(true)
    try {
      for (const bundle of bundles) {
        const fileMap = new Map<string, File>()
        fileMap.set(bundle.audioFile.name, bundle.audioFile)
        fileMap.set(bundle.chartFile.name, bundle.chartFile)
        const placeholder = placeholderImageFile()
        fileMap.set((bundle.bgFile ?? placeholder).name, bundle.bgFile ?? placeholder)
        fileMap.set(
          (bundle.bannerFile ?? placeholder).name,
          bundle.bannerFile ?? placeholder,
        )

        const meta: CustomSongMeta = {
          id: bundle.id,
          title: bundle.chart.title,
          artist: bundle.chart.artist,
          bpm: bundle.chart.bpm,
          difficulty: Math.min(5, Math.max(1, Math.round(
            (bundle.chart.difficulties.find((d) => d.difficulty === bundle.selectedDifficulty)?.meter ?? 5) / 2.4,
          ))),
          duration: estimateDuration(bundle.chart),
          genre: 'Custom',
          color: PALETTE[Math.floor(Math.random() * PALETTE.length)],
          selectedDifficulty: bundle.selectedDifficulty,
          simplify: bundle.simplify,
          audioFile: bundle.audioFile.name,
          chartFile: bundle.chartFile.name,
          bgFile: (bundle.bgFile ?? placeholder).name,
          bannerFile: (bundle.bannerFile ?? placeholder).name,
        }

        await addSong(meta, fileMap, storeInBrowser)
      }

      navigate(ROUTE_PATHS.songSelection)
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Failed to add songs')
    } finally {
      setProcessing(false)
    }
  }, [bundles, storeInBrowser, addSong, navigate])

  return (
    <div className={styles.root}>
      <button className={styles.backBtn} onClick={() => navigate(ROUTE_PATHS.songSelection)}>
        &larr; BACK
      </button>

      <h1 className={`${styles.title} font-title`}>ADD TRACK</h1>
      <p className={styles.subtitle}>
        Everything stays on your device &mdash; nothing is uploaded or shared
      </p>

      <div
        className={`${styles.dropZone} ${dragOver ? styles.dropZoneDragOver : ''}`}
        onDrop={handleDrop}
        onDragOver={handleDragOver}
        onDragLeave={handleDragLeave}
        onClick={() => fileInputRef.current?.click()}
        role="button"
        tabIndex={0}
      >
        <div className={styles.dropIcon}>+</div>
        <div className={styles.dropLabelDesktop}>
          Drag & drop files here, or click to browse
        </div>
        <div className={styles.dropLabelMobile}>
          Tap to choose files
        </div>
        <div className={styles.dropRequirements}>
          <span className={styles.dropRequired}>Required:</span> audio (.ogg, .mp3, .wav) + chart (.ssc/.sm)
        </div>
        <div className={styles.dropHint}>
          Optional: background (bg.png) &middot; banner (bn.png)
        </div>
        <div className={styles.dropHintDesktop}>
          Or drop a .zip with one or more song folders
        </div>
      </div>
      <input
        ref={fileInputRef}
        type="file"
        className={styles.fileInput}
        multiple
        accept=".ssc,.sm,.ogg,.mp3,.wav,.zip,.png,.jpg,.jpeg"
        onChange={handleFileInput}
      />

      {processing && <div className={styles.loading}>Processing...</div>}
      {error && <div className={styles.error}>{error}</div>}

      {bundles.length > 0 && (
        <>
          <div className={styles.songList}>
            {bundles.map((bundle) => (
              <div key={bundle.id} className={styles.songCard}>
                <div className={styles.songHeader}>
                  {bundle.bgFile && (
                    <img
                      className={styles.songBanner}
                      src={URL.createObjectURL(bundle.bgFile)}
                      alt=""
                    />
                  )}
                  <div className={styles.songInfo}>
                    <h3 className={`${styles.songTitle} font-title`}>
                      {bundle.chart.title}
                    </h3>
                    <p className={styles.songArtist}>{bundle.chart.artist}</p>
                    <p className={styles.songMeta}>
                      {bundle.chart.bpm} BPM &middot; {estimateDuration(bundle.chart)}
                    </p>
                  </div>
                  <button
                    className={styles.songRemoveBtn}
                    onClick={() => removeBundle(bundle.id)}
                    title="Remove"
                  >
                    &times;
                  </button>
                </div>

                <div className={styles.options}>
                  <label className={styles.option}>
                    Difficulty
                    <select
                      value={bundle.selectedDifficulty}
                      onChange={(e) =>
                        updateBundle(bundle.id, { selectedDifficulty: e.target.value })
                      }
                    >
                      {bundle.chart.difficulties.map((d) => (
                        <option key={d.difficulty} value={d.difficulty}>
                          {d.difficulty} ({d.meter})
                        </option>
                      ))}
                    </select>
                  </label>

                  <label
                    className={styles.optionWithHelp}
                    data-tooltip="Many StepMania charts require stepping in two directions at once. Since this game uses body tracking (not a dance pad), only one step can be detected at a time. Simplify removes simultaneous arrows so every note is hittable."
                  >
                    <input
                      type="checkbox"
                      checked={bundle.simplify}
                      onChange={(e) =>
                        updateBundle(bundle.id, { simplify: e.target.checked })
                      }
                    />
                    Simplify (no simultaneous arrows)
                  </label>
                </div>
              </div>
            ))}
          </div>

          <div className={styles.actions}>
            <label
              className={styles.optionWithHelp}
              data-tooltip="Saves song files to your browser so they persist across sessions. Without this, uploaded tracks are lost when you close the tab. Uses disk space on this device."
            >
              <input
                type="checkbox"
                checked={storeInBrowser}
                onChange={(e) => setStoreInBrowser(e.target.checked)}
              />
              Store in browser
            </label>
            <button
              className={`${styles.addBtn} font-title`}
              disabled={processing || bundles.length === 0}
              onClick={handleAdd}
            >
              {processing ? 'ADDING...' : `ADD ${bundles.length} TRACK${bundles.length > 1 ? 'S' : ''}`}
            </button>
          </div>
        </>
      )}
    </div>
  )
}
