package spotify

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"qtransfer/internal/model"
)

const baseURL = "https://api.spotify.com/v1"

type Client struct {
	httpClient *http.Client
	token      string
	progress   ProgressFunc
}

type ProgressFunc func(message string)

type User struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name"`
}

func NewClient(token string) *Client {
	return &Client{
		httpClient: &http.Client{Timeout: 30 * time.Second},
		token:      token,
	}
}

func (c *Client) SetProgress(fn ProgressFunc) {
	c.progress = fn
}

func (c *Client) GetCurrentUser(ctx context.Context) (User, error) {
	var u User
	err := c.getJSON(ctx, baseURL+"/me", &u)
	return u, err
}

func (c *Client) FetchLibrary(ctx context.Context, likedName string) (model.Library, error) {
	c.notifyProgress("Fetching Spotify profile...")
	user, err := c.GetCurrentUser(ctx)
	if err != nil {
		return model.Library{}, err
	}

	c.notifyProgress("Fetching Spotify playlists...")
	pls, err := c.FetchPlaylists(ctx)
	if err != nil {
		return model.Library{}, err
	}

	c.notifyProgress("Fetching Spotify liked songs...")
	liked, err := c.FetchLikedSongs(ctx)
	if err != nil {
		return model.Library{}, err
	}

	lib := model.Library{
		UserID:       user.ID,
		DisplayName:  user.DisplayName,
		Playlists:    pls,
		LikedSongs:   liked,
		LikedName:    likedName,
		SourceSystem: "spotify",
	}

	return lib, nil
}

func (c *Client) FetchPlaylists(ctx context.Context) ([]model.Playlist, error) {
	type playlistLite struct {
		ID          string `json:"id"`
		Name        string `json:"name"`
		Description string `json:"description"`
	}
	type page struct {
		Items []playlistLite `json:"items"`
		Next  string         `json:"next"`
		Total int            `json:"total"`
	}

	var out []model.Playlist
	next := baseURL + "/me/playlists?limit=50"
	loadedPlaylists := 0
	totalPlaylists := 0
	for next != "" {
		var p page
		if err := c.getJSON(ctx, next, &p); err != nil {
			return nil, err
		}
		if p.Total > 0 {
			totalPlaylists = p.Total
		}
		for _, item := range p.Items {
			loadedPlaylists++
			if totalPlaylists > 0 {
				c.notifyProgress(fmt.Sprintf("Spotify playlists: %d/%d", loadedPlaylists, totalPlaylists))
			} else {
				c.notifyProgress(fmt.Sprintf("Spotify playlists: %d", loadedPlaylists))
			}

			tracks, err := c.fetchPlaylistTracks(ctx, item.ID, item.Name, loadedPlaylists, totalPlaylists)
			if err != nil {
				return nil, fmt.Errorf("fetch playlist tracks %s: %w", item.Name, err)
			}
			out = append(out, model.Playlist{
				SourceID:    item.ID,
				Name:        item.Name,
				Description: item.Description,
				Tracks:      tracks,
			})
		}
		next = p.Next
	}

	return out, nil
}

func (c *Client) FetchPlaylistsByID(ctx context.Context, ids []string) ([]model.Playlist, error) {
	out := make([]model.Playlist, 0, len(ids))
	for i, id := range ids {
		pl, err := c.FetchPlaylistByID(ctx, id)
		if err != nil {
			return nil, err
		}
		c.notifyProgress(fmt.Sprintf("Spotify playlist urls: %d/%d", i+1, len(ids)))
		out = append(out, pl)
	}
	return out, nil
}

func (c *Client) FetchPlaylistByID(ctx context.Context, playlistID string) (model.Playlist, error) {
	type response struct {
		ID          string `json:"id"`
		Name        string `json:"name"`
		Description string `json:"description"`
	}

	endpoint := fmt.Sprintf("%s/playlists/%s?fields=id,name,description", baseURL, url.PathEscape(playlistID))
	var meta response
	if err := c.getJSON(ctx, endpoint, &meta); err != nil {
		return model.Playlist{}, fmt.Errorf("fetch playlist metadata %s: %w", playlistID, err)
	}

	tracks, err := c.fetchPlaylistTracks(ctx, meta.ID, meta.Name, 0, 0)
	if err != nil {
		return model.Playlist{}, fmt.Errorf("fetch playlist tracks %s: %w", meta.Name, err)
	}

	return model.Playlist{
		SourceID:    meta.ID,
		Name:        meta.Name,
		Description: meta.Description,
		Tracks:      tracks,
	}, nil
}

func (c *Client) fetchPlaylistTracks(ctx context.Context, playlistID, playlistName string, playlistIndex, playlistTotal int) ([]model.Track, error) {
	type trackObj struct {
		ID          string `json:"id"`
		Name        string `json:"name"`
		DurationMS  int    `json:"duration_ms"`
		Explicit    bool   `json:"explicit"`
		ExternalIDs struct {
			ISRC string `json:"isrc"`
		} `json:"external_ids"`
		Album struct {
			Name string `json:"name"`
		} `json:"album"`
		Artists []struct {
			Name string `json:"name"`
		} `json:"artists"`
	}
	type item struct {
		Track *trackObj `json:"track"`
	}
	type page struct {
		Items []item `json:"items"`
		Next  string `json:"next"`
		Total int    `json:"total"`
	}

	var out []model.Track
	next := fmt.Sprintf("%s/playlists/%s/tracks?limit=100", baseURL, url.PathEscape(playlistID))
	loadedTracks := 0
	totalTracks := 0
	for next != "" {
		var p page
		if err := c.getJSON(ctx, next, &p); err != nil {
			return nil, err
		}
		if p.Total > 0 {
			totalTracks = p.Total
		}
		for _, it := range p.Items {
			if it.Track == nil || it.Track.ID == "" {
				continue
			}
			loadedTracks++
			out = append(out, toModelTrack(
				it.Track.ID,
				it.Track.Name,
				it.Track.Album.Name,
				it.Track.DurationMS,
				it.Track.ExternalIDs.ISRC,
				it.Track.Explicit,
				it.Track.Artists,
			))
		}

		prefix := "Playlist"
		if playlistTotal > 0 {
			prefix = fmt.Sprintf("Playlist %d/%d", playlistIndex, playlistTotal)
		} else if playlistIndex > 0 {
			prefix = fmt.Sprintf("Playlist %d", playlistIndex)
		}

		if totalTracks > 0 {
			c.notifyProgress(fmt.Sprintf("%s (%s): tracks %d/%d", prefix, playlistName, loadedTracks, totalTracks))
		} else {
			c.notifyProgress(fmt.Sprintf("%s (%s): tracks %d", prefix, playlistName, loadedTracks))
		}

		next = p.Next
	}

	return out, nil
}

func (c *Client) FetchLikedSongs(ctx context.Context) ([]model.Track, error) {
	type trackObj struct {
		ID          string `json:"id"`
		Name        string `json:"name"`
		DurationMS  int    `json:"duration_ms"`
		Explicit    bool   `json:"explicit"`
		ExternalIDs struct {
			ISRC string `json:"isrc"`
		} `json:"external_ids"`
		Album struct {
			Name string `json:"name"`
		} `json:"album"`
		Artists []struct {
			Name string `json:"name"`
		} `json:"artists"`
	}
	type item struct {
		Track *trackObj `json:"track"`
	}
	type page struct {
		Items []item `json:"items"`
		Next  string `json:"next"`
		Total int    `json:"total"`
	}

	var out []model.Track
	next := baseURL + "/me/tracks?limit=50"
	loaded := 0
	total := 0
	for next != "" {
		var p page
		if err := c.getJSON(ctx, next, &p); err != nil {
			return nil, err
		}
		if p.Total > 0 {
			total = p.Total
		}
		for _, it := range p.Items {
			if it.Track == nil || it.Track.ID == "" {
				continue
			}
			loaded++
			out = append(out, toModelTrack(
				it.Track.ID,
				it.Track.Name,
				it.Track.Album.Name,
				it.Track.DurationMS,
				it.Track.ExternalIDs.ISRC,
				it.Track.Explicit,
				it.Track.Artists,
			))
		}

		if total > 0 {
			c.notifyProgress(fmt.Sprintf("Liked songs: %d/%d", loaded, total))
		} else {
			c.notifyProgress(fmt.Sprintf("Liked songs: %d", loaded))
		}

		next = p.Next
	}

	return out, nil
}

func (c *Client) notifyProgress(msg string) {
	if c.progress != nil {
		c.progress(msg)
	}
}

func toModelTrack(id, name, album string, durationMS int, isrc string, explicit bool, artists []struct {
	Name string "json:\"name\""
}) model.Track {
	artistNames := make([]string, 0, len(artists))
	for _, a := range artists {
		if strings.TrimSpace(a.Name) != "" {
			artistNames = append(artistNames, a.Name)
		}
	}
	return model.Track{
		SourceID:   id,
		Title:      name,
		Artists:    artistNames,
		Album:      album,
		DurationMS: durationMS,
		ISRC:       strings.ToUpper(strings.TrimSpace(isrc)),
		Explicit:   explicit,
	}
}

func (c *Client) getJSON(ctx context.Context, endpoint string, out any) error {
	var lastErr error
	for attempt := 1; attempt <= 4; attempt++ {
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
		if err != nil {
			return err
		}
		req.Header.Set("Authorization", "Bearer "+c.token)

		resp, err := c.httpClient.Do(req)
		if err != nil {
			lastErr = err
			time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
			continue
		}

		if resp.StatusCode == http.StatusTooManyRequests {
			resp.Body.Close()
			time.Sleep(time.Duration(attempt) * time.Second)
			continue
		}

		if resp.StatusCode >= 500 {
			resp.Body.Close()
			time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
			continue
		}

		if resp.StatusCode >= 300 {
			b, _ := io.ReadAll(resp.Body)
			resp.Body.Close()
			return fmt.Errorf("spotify api error (%d): %s", resp.StatusCode, strings.TrimSpace(string(b)))
		}

		defer resp.Body.Close()
		if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
			return err
		}
		return nil
	}

	if lastErr == nil {
		lastErr = fmt.Errorf("spotify request failed after retries")
	}
	return lastErr
}
