package main

import (
	"encoding/json"
	"fmt"
	"math/rand/v2"
	"net/http"
	"strings"
	"time"

	"github.com/joho/godotenv"
)

const numVoteStyles = 4

var songStyles = []string{
	"Chill", "Energetic", "Party", "Ambient", "Lo-fi", "Jazz", "Electronic", "Tropical House", "Deep House", "Techno", "Trance", "Dubstep", "Drum & Bass", "Synthwave", "Vaporwave", "Future Bass", "Trap", "Hardstyle", "Progressive House", "Minimal", "Downtempo", "Chillstep", "Liquid DnB", "Psytrance", "Breakbeat", "Garage", "Afrobeat", "Latin", "Reggaeton", "Bossa Nova", "Salsa", "Flamenco", "Celtic", "Nordic", "Mediterranean", "Middle Eastern", "Asian Fusion", "Bollywood", "K-Pop", "J-Pop", "City Pop", "Funk", "Disco", "Soul", "R&B", "Gospel", "Blues", "Rock", "Indie", "Folk", "Country", "Bluegrass", "Classical", "Baroque", "Romantic", "Modern Classical", "Cinematic", "Epic", "Dark", "Ethereal", "Mystical", "Retro", "80s", "90s", "2000s", "Acoustic", "Piano", "Orchestral", "String Quartet", "Brass", "Woodwind", "Percussive", "Nature", "Ocean", "Forest", "Urban", "Industrial", "Glitch", "Noise", "Drone", "Meditation", "Yoga", "Spa", "Study", "Sleep", "Morning", "Sunset", "Night", "Space", "Cyberpunk", "Steampunk", "Fantasy", "8-bit", "Chipwave", "Kawaii", "Melancholic", "Euphoric", "Aggressive", "Experimental", "House",
}

var startingSong = SongState{
	songUrl:     "https://cdn1.suno.ai/15081bda-1eb1-4b39-919d-5814675f4138.mp3",
	imageUrl:    "https://cdn1.suno.ai/image_large_15081bda-1eb1-4b39-919d-5814675f4138.jpeg",
	title:       "Starting Song",
	description: "This is the starting song",
	tags:        []string{"test", "song"},
	startTime:   time.Now(),
	duration:    175880 * time.Millisecond,
}

type SongState struct {
	songUrl     string
	imageUrl    string
	title       string
	description string
	tags        []string
	startTime   time.Time
	duration    time.Duration
}

type RadioState struct {
	styles    [numVoteStyles]string
	votes     [numVoteStyles]int
	voteChan  chan string
	clearChan chan bool
	songList  []SongState
}

func getRandomStyle() string {
	return songStyles[rand.IntN(len(songStyles))]
}

func getRandomStyleList(numStyles int) []string {
	set := make(map[string]bool)
	result := make([]string, 0, numStyles)
	for len(result) < numStyles {
		style := getRandomStyle()
		if !set[style] {
			set[style] = true
			result = append(result, style)
		}
	}
	return result
}

func (r *RadioState) Init() {
	r.voteChan = make(chan string)
	r.clearChan = make(chan bool)
	r.songList = []SongState{startingSong}
	r.reset()
}

func (r *RadioState) reset() {
	var styles [numVoteStyles]string
	copy(styles[:], getRandomStyleList(numVoteStyles))
	r.styles = styles
	r.votes = [numVoteStyles]int{0, 0, 0, 0}
}

func (r *RadioState) vote(vote string) {
	for i, style := range r.styles {
		if style == vote {
			r.votes[i]++
		}
	}
}

func (r *RadioState) VoteManager() {
	for {
		select {
		case vote := <-r.voteChan:
			r.vote(vote)
		case <-r.clearChan:
			r.reset()
		}
	}
}

func (radio *RadioState) Server() {
	http.HandleFunc("/vote", func(w http.ResponseWriter, r *http.Request) {
		// Add CORS headers
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

		if r.Method == "OPTIONS" {
			return
		}
		if r.Method != "POST" {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}
		radio.voteChan <- r.FormValue("vote")
	})

	http.HandleFunc("/song-list", func(w http.ResponseWriter, r *http.Request) {
		// Add CORS headers
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

		if r.Method == "OPTIONS" {
			return
		}
		if r.Method != "GET" {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}
		// Only return the last 10 songs (or fewer)
		songList := radio.songList
		if len(songList) > 10 {
			songList = songList[len(songList)-10:]
		}
		// Prepare a struct for JSON output (exported fields)
		type SongInfo struct {
			SongUrl     string    `json:"songUrl"`
			ImageUrl    string    `json:"imageUrl"`
			Title       string    `json:"title"`
			Description string    `json:"description"`
			Tags        []string  `json:"tags"`
			StartTime   time.Time `json:"startTime"`
			Duration    float64   `json:"duration"`
		}
		result := make([]SongInfo, len(songList))
		for i, s := range songList {
			result[i] = SongInfo{
				SongUrl:     s.songUrl,
				ImageUrl:    s.imageUrl,
				Title:       s.title,
				Description: s.description,
				Tags:        s.tags,
				StartTime:   s.startTime,
				Duration:    s.duration.Seconds(),
			}
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(result)
	})

	http.HandleFunc("/vote-status", func(w http.ResponseWriter, r *http.Request) {
		// Add CORS headers
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

		if r.Method == "OPTIONS" {
			return
		}
		if r.Method != "GET" {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}
		// Prepare a struct for JSON output
		type VoteStatus struct {
			Styles       []string   `json:"styles"`
			Votes        []int      `json:"votes"`
			VotesCloseAt *time.Time `json:"votesCloseAt"`
		}
		styles := make([]string, numVoteStyles)
		copy(styles, radio.styles[:])
		votes := make([]int, numVoteStyles)
		copy(votes, radio.votes[:])

		// Calculate when votes will close (90 seconds before current song ends)
		var votesCloseAt *time.Time
		if len(radio.songList) > 0 {
			lastSong := radio.songList[len(radio.songList)-1]
			songEndTime := lastSong.startTime.Add(lastSong.duration)
			now := time.Now()

			// If we're in the 90-second period after song ends, votes are closed (null)
			if now.After(songEndTime.Add(-90 * time.Second)) {
				votesCloseAt = nil
			} else {
				closeTime := songEndTime.Add(-90 * time.Second)
				votesCloseAt = &closeTime
			}
		}

		status := VoteStatus{
			Styles:       styles,
			Votes:        votes,
			VotesCloseAt: votesCloseAt,
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(status)
	})

	http.ListenAndServe(":8080", nil)
}

func (r *RadioState) MainLoop() {
	for {
		if len(r.songList) == 0 {
			panic("No songs in songList")
		}
		lastSong := r.songList[len(r.songList)-1]
		endTime := lastSong.startTime.Add(lastSong.duration)
		now := time.Now()
		if now.After(endTime) {
			panic("Song is already over")
		}
		waitDuration := endTime.Add(-90 * time.Second).Sub(now)
		if waitDuration > 0 {
			time.Sleep(waitDuration)
		}
		prompt, title := r.GeneratePrompt()
		r.clearChan <- true // clear the votes
		audioURL, imageLargeURL, duration, _, tags := GenerateSongRequest(prompt)
		newSong := SongState{
			songUrl:     audioURL,
			imageUrl:    imageLargeURL,
			title:       title,
			description: prompt,
			tags:        tags,
			startTime:   endTime,
			duration:    time.Duration(duration * float64(time.Second)),
		}
		r.songList = append(r.songList, newSong)
		if len(r.songList) > 100 { // to prevent the songList from growing too large
			newList := make([]SongState, len(r.songList)-50)
			copy(newList, r.songList[50:])
			r.songList = newList
		}
	}
}

// GeneratePrompt generates a prompt string using the current time, date, and the current votes and styles.
func (r *RadioState) GeneratePrompt() (string, string) {
	// Get current time and date
	now := time.Now()
	// Time of day description
	hour := now.Hour()
	timeOfDay := ""
	switch {
	case hour < 6:
		timeOfDay = "late night"
	case hour < 12:
		timeOfDay = "early morning"
	case hour < 17:
		timeOfDay = "afternoon"
	case hour < 21:
		timeOfDay = "evening"
	default:
		timeOfDay = "night"
	}
	// Day of week and month
	weekday := strings.ToLower(now.Weekday().String())
	month := now.Month().String()
	// Use r.votes and r.styles directly
	totalVotes := 0
	for _, v := range r.votes {
		totalVotes += v
	}
	// If no votes, just use equal percentages
	percentages := make([]int, numVoteStyles)
	if totalVotes == 0 {
		for i := range percentages {
			percentages[i] = 100 / numVoteStyles
		}
	} else {
		for i, v := range r.votes {
			percentages[i] = int(float64(v) / float64(totalVotes) * 100)
		}
	}
	// Build style/percent string
	styleParts := make([]string, 0, numVoteStyles)
	for i, style := range r.styles {
		styleParts = append(styleParts, fmt.Sprintf("%d%% %s", percentages[i], strings.ToLower(style)))
	}
	styleStr := strings.Join(styleParts, ", ")
	// Compose prompt
	prompt := fmt.Sprintf("An instrumental track capturing the feeling of %s on a %s, in %s, that is %s.", timeOfDay, weekday, month, styleStr)
	title := fmt.Sprintf("%s on a %s", timeOfDay, weekday)
	return prompt, title
}

func main() {
	err := godotenv.Load()
	if err != nil {
		panic("Error loading .env file")
	}
	radio := RadioState{}
	radio.Init()
	go radio.VoteManager()
	go radio.MainLoop()
	radio.Server()
}
