package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

// ── MusicBrainz types ─────────────────────────────────────────────────────────

type mbArtistCredit struct {
	Name   string `json:"name"`
	Artist struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	} `json:"artist"`
}

type mbRelease struct {
	ID           string           `json:"id"`
	Title        string           `json:"title"`
	Date         string           `json:"date"`
	ArtistCredit []mbArtistCredit `json:"artist-credit"`
	ReleaseGroup struct {
		PrimaryType string `json:"primary-type"`
	} `json:"release-group"`
}

type mbArtist struct {
	ID             string `json:"id"`
	Name           string `json:"name"`
	Country        string `json:"country"`
	Disambiguation string `json:"disambiguation"`
}

type mbReleaseGroup struct {
	ID               string `json:"id"`
	Title            string `json:"title"`
	PrimaryType      string `json:"primary-type"`
	FirstReleaseDate string `json:"first-release-date"`
}

func mbGet(path string, out interface{}) error {
	req, err := http.NewRequest("GET", "https://musicbrainz.org"+path, nil)
	if err != nil {
		return err
	}
	req.Header.Set("User-Agent", "music-importer/1.0 (https://github.com/gabehf/music-importer)")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("MusicBrainz returned %d", resp.StatusCode)
	}
	return json.NewDecoder(resp.Body).Decode(out)
}

func searchMBReleases(query string) ([]mbRelease, error) {
	var result struct {
		Releases []mbRelease `json:"releases"`
	}
	err := mbGet("/ws/2/release/?query="+url.QueryEscape(query)+"&fmt=json&limit=20", &result)
	return result.Releases, err
}

func searchMBArtists(query string) ([]mbArtist, error) {
	var result struct {
		Artists []mbArtist `json:"artists"`
	}
	err := mbGet("/ws/2/artist/?query="+url.QueryEscape(query)+"&fmt=json&limit=20", &result)
	return result.Artists, err
}

// getMBArtistReleaseGroups returns all Album and EP release groups for an artist,
// paginating through the MusicBrainz browse API with the required 1 req/s rate limit.
func getMBArtistReleaseGroups(artistMBID string) ([]mbReleaseGroup, error) {
	const limit = 100
	var all []mbReleaseGroup

	for offset := 0; ; offset += limit {
		path := fmt.Sprintf(
			"/ws/2/release-group?artist=%s&type=album%%7Cep&fmt=json&limit=%d&offset=%d",
			url.QueryEscape(artistMBID), limit, offset,
		)

		var result struct {
			ReleaseGroups []mbReleaseGroup `json:"release-groups"`
			Count         int              `json:"release-group-count"`
		}
		if err := mbGet(path, &result); err != nil {
			return all, err
		}

		for _, rg := range result.ReleaseGroups {
			t := strings.ToLower(rg.PrimaryType)
			if t == "album" || t == "ep" {
				all = append(all, rg)
			}
		}

		if offset+limit >= result.Count {
			break
		}
		time.Sleep(time.Second) // MusicBrainz rate limit
	}

	return all, nil
}

// fetchArtist fetches every Album and EP release group for an artist by running
// fetchRelease for each one sequentially, then registers each for monitoring.
func fetchArtist(artistMBID, artistName string, logf func(string)) error {
	log.Printf("[discover] artist fetch started: %s (%s)", artistName, artistMBID)
	logf(fmt.Sprintf("Looking up discography for %s on MusicBrainz…", artistName))

	groups, err := getMBArtistReleaseGroups(artistMBID)
	if err != nil {
		return fmt.Errorf("MusicBrainz discography lookup failed: %w", err)
	}
	if len(groups) == 0 {
		return fmt.Errorf("no albums or EPs found for %s on MusicBrainz", artistName)
	}

	log.Printf("[discover] found %d release groups for %s", len(groups), artistName)
	logf(fmt.Sprintf("Found %d albums/EPs", len(groups)))

	failed := 0
	for i, rg := range groups {
		logf(fmt.Sprintf("[%d/%d] %s", i+1, len(groups), rg.Title))
		folder, err := fetchRelease(artistName, rg.Title, rg.ID, logf)
		if err != nil {
			log.Printf("[discover] fetch failed for %q by %s: %v", rg.Title, artistName, err)
			logf(fmt.Sprintf("  ↳ failed: %v", err))
			failed++
			continue
		}
		registerDownload(rg.ID, artistName, rg.Title, folder, nil)
		logf(fmt.Sprintf("  ↳ registered for import (mbid: %s)", rg.ID))
	}

	if failed > 0 {
		logf(fmt.Sprintf("Done — %d/%d queued, %d failed", len(groups)-failed, len(groups), failed))
	} else {
		logf(fmt.Sprintf("Done — all %d downloads queued, monitoring for import", len(groups)))
	}
	log.Printf("[discover] artist fetch complete: %s (%d/%d succeeded)", artistName, len(groups)-failed, len(groups))
	return nil
}

// ── Fetch state ───────────────────────────────────────────────────────────────

type fetchEntry struct {
	mu      sync.Mutex
	ID      string   `json:"id"`
	Artist  string   `json:"artist"`
	Album   string   `json:"album"`
	Log     []string `json:"log"`
	Done    bool     `json:"done"`
	Success bool     `json:"success"`
	ErrMsg  string   `json:"error,omitempty"`
}

var (
	fetchesMu sync.Mutex
	fetchMap  = make(map[string]*fetchEntry)
)

func newFetchEntry(id, artist, album string) *fetchEntry {
	e := &fetchEntry{ID: id, Artist: artist, Album: album}
	fetchesMu.Lock()
	fetchMap[id] = e
	fetchesMu.Unlock()
	return e
}

func (e *fetchEntry) appendLog(msg string) {
	e.mu.Lock()
	e.Log = append(e.Log, msg)
	e.mu.Unlock()
}

func (e *fetchEntry) finish(err error) {
	e.mu.Lock()
	e.Done = true
	if err != nil {
		e.ErrMsg = err.Error()
	} else {
		e.Success = true
	}
	e.mu.Unlock()
}

func (e *fetchEntry) snapshot() fetchEntry {
	e.mu.Lock()
	defer e.mu.Unlock()
	cp := *e
	cp.Log = append([]string(nil), e.Log...)
	return cp
}

// ── HTTP handlers ─────────────────────────────────────────────────────────────

// handleDiscoverSearch handles GET /discover/search?q=...&type=release|artist
func handleDiscoverSearch(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query().Get("q")
	if q == "" {
		http.Error(w, "missing q", http.StatusBadRequest)
		return
	}
	searchType := r.URL.Query().Get("type")
	if searchType == "" {
		searchType = "release"
	}
	log.Printf("[discover] search: type=%s q=%q", searchType, q)

	w.Header().Set("Content-Type", "application/json")

	switch searchType {
	case "artist":
		artists, err := searchMBArtists(q)
		if err != nil {
			log.Printf("[discover] artist search error: %v", err)
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		log.Printf("[discover] artist search returned %d results", len(artists))
		json.NewEncoder(w).Encode(artists)

	default: // "release"
		releases, err := searchMBReleases(q)
		if err != nil {
			log.Printf("[discover] release search error: %v", err)
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		log.Printf("[discover] release search returned %d results", len(releases))
		json.NewEncoder(w).Encode(releases)
	}
}

// handleDiscoverFetch handles POST /discover/fetch
// Body: {"id":"mbid","artist":"...","album":"..."}
func handleDiscoverFetch(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}

	var body struct {
		ID     string `json:"id"`
		Artist string `json:"artist"`
		Album  string `json:"album"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == "" || body.Artist == "" || body.Album == "" {
		http.Error(w, "id, artist and album are required", http.StatusBadRequest)
		return
	}

	// If a fetch for this ID is already in progress, return its ID without starting a new one.
	fetchesMu.Lock()
	existing := fetchMap[body.ID]
	fetchesMu.Unlock()
	if existing != nil {
		existing.mu.Lock()
		done := existing.Done
		existing.mu.Unlock()
		if !done {
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]string{"id": body.ID})
			return
		}
	}

	log.Printf("[discover] starting fetch: %q by %s (mbid: %s)", body.Album, body.Artist, body.ID)
	entry := newFetchEntry(body.ID, body.Artist, body.Album)
	go func() {
		folder, err := fetchRelease(body.Artist, body.Album, body.ID, entry.appendLog)
		if err != nil {
			log.Printf("[discover] fetch failed for %q by %s: %v", body.Album, body.Artist, err)
			entry.finish(err)
			return
		}
		log.Printf("[discover] fetch complete for %q by %s, registering for import", body.Album, body.Artist)
		registerDownload(body.ID, body.Artist, body.Album, folder, entry)
		// entry.finish is called by the monitor when import completes
	}()

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"id": body.ID})
}

// handleDiscoverFetchArtist handles POST /discover/fetch/artist
// Body: {"id":"artist-mbid","name":"Artist Name"}
func handleDiscoverFetchArtist(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}

	var body struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == "" || body.Name == "" {
		http.Error(w, "id and name are required", http.StatusBadRequest)
		return
	}

	fetchesMu.Lock()
	existing := fetchMap[body.ID]
	fetchesMu.Unlock()
	if existing != nil {
		existing.mu.Lock()
		done := existing.Done
		existing.mu.Unlock()
		if !done {
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]string{"id": body.ID})
			return
		}
	}

	log.Printf("[discover] starting artist fetch: %s (%s)", body.Name, body.ID)
	entry := newFetchEntry(body.ID, body.Name, "")
	go func() {
		err := fetchArtist(body.ID, body.Name, entry.appendLog)
		if err != nil {
			log.Printf("[discover] artist fetch failed for %s: %v", body.Name, err)
		} else {
			log.Printf("[discover] artist fetch complete for %s", body.Name)
		}
		entry.finish(err)
	}()

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"id": body.ID})
}

// handleDiscoverFetchStatus handles GET /discover/fetch/status?id=...
func handleDiscoverFetchStatus(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")
	if id == "" {
		http.Error(w, "missing id", http.StatusBadRequest)
		return
	}

	fetchesMu.Lock()
	entry := fetchMap[id]
	fetchesMu.Unlock()

	if entry == nil {
		http.Error(w, "not found", http.StatusNotFound)
		return
	}

	snap := entry.snapshot()
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(snap)
}

// fetchListItem is a summary of a fetch entry for the list endpoint.
type fetchListItem struct {
	ID      string `json:"id"`
	Title   string `json:"title"`
	Done    bool   `json:"done"`
	Success bool   `json:"success"`
}

// handleDiscoverFetchList handles GET /discover/fetch/list
// Returns a summary of all known fetch entries so the frontend can discover
// entries created server-side (e.g. per-album entries from an artist fetch).
func handleDiscoverFetchList(w http.ResponseWriter, r *http.Request) {
	fetchesMu.Lock()
	items := make([]fetchListItem, 0, len(fetchMap))
	for _, e := range fetchMap {
		e.mu.Lock()
		title := e.Artist
		if e.Album != "" {
			title = e.Artist + " \u2014 " + e.Album
		}
		items = append(items, fetchListItem{
			ID:      e.ID,
			Title:   title,
			Done:    e.Done,
			Success: e.Success,
		})
		e.mu.Unlock()
	}
	fetchesMu.Unlock()

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(items)
}
