package igdb

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

	"github.com/tdvorak/seen/backend/internal/config"
	"github.com/tdvorak/seen/backend/internal/services/catalog"
)

var ErrIGDBCredentialsMissing = errors.New("igdb client credentials missing")

type Client struct {
	cfg        config.IGDBConfig
	httpClient *http.Client

	mu             sync.Mutex
	accessToken    string
	accessTokenExp time.Time
}

type tokenResponse struct {
	AccessToken string `json:"access_token"`
	ExpiresIn   int    `json:"expires_in"`
	TokenType   string `json:"token_type"`
}

type gameResponse struct {
	ID               int64   `json:"id"`
	Name             string  `json:"name"`
	Summary          string  `json:"summary"`
	FirstReleaseDate int64   `json:"first_release_date"`
	Rating           float64 `json:"rating"`
	Genres           []struct {
		Name string `json:"name"`
	} `json:"genres"`
	Platforms []struct {
		Name string `json:"name"`
	} `json:"platforms"`
}

func NewClient(cfg config.IGDBConfig) *Client {
	return &Client{
		cfg: cfg,
		httpClient: &http.Client{
			Timeout: 12 * time.Second,
		},
	}
}

func (c *Client) Enabled() bool {
	return strings.TrimSpace(c.cfg.ClientID) != "" && strings.TrimSpace(c.cfg.ClientSecret) != ""
}

func (c *Client) SearchGames(ctx context.Context, query string, limit int) ([]catalog.MediaItem, error) {
	if !c.Enabled() {
		return nil, ErrIGDBCredentialsMissing
	}

	cleanQuery := strings.TrimSpace(query)
	if cleanQuery == "" {
		return []catalog.MediaItem{}, nil
	}

	if limit < 1 {
		limit = 12
	}

	token, err := c.token(ctx)
	if err != nil {
		return nil, err
	}

	body := fmt.Sprintf(
		"fields id,name,summary,first_release_date,rating,genres.name,platforms.name; search %q; limit %d;",
		cleanQuery,
		limit,
	)

	request, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		strings.TrimRight(c.cfg.BaseURL, "/")+"/games",
		strings.NewReader(body),
	)
	if err != nil {
		return nil, err
	}

	request.Header.Set("Client-ID", c.cfg.ClientID)
	request.Header.Set("Authorization", "Bearer "+token)
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Content-Type", "text/plain")

	response, err := c.httpClient.Do(request)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()

	if response.StatusCode >= http.StatusBadRequest {
		message, _ := io.ReadAll(io.LimitReader(response.Body, 2048))
		return nil, fmt.Errorf("igdb search failed: %s", strings.TrimSpace(string(message)))
	}

	var payload []gameResponse
	if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
		return nil, err
	}

	items := make([]catalog.MediaItem, 0, len(payload))
	for _, game := range payload {
		title := strings.TrimSpace(game.Name)
		if title == "" {
			continue
		}

		overview := strings.TrimSpace(game.Summary)
		if overview == "" {
			overview = fmt.Sprintf("%s is available through the live IGDB provider search.", title)
		}

		items = append(items, catalog.MediaItem{
			ID:             int(game.ID) + 900000,
			Provider:       catalog.MediaProviderIGDB,
			ProviderID:     int(game.ID),
			Title:          title,
			Overview:       overview,
			Type:           catalog.MediaTypeGame,
			ReleaseDate:    unixDate(game.FirstReleaseDate),
			Genres:         genreNames(game.Genres),
			Platforms:      platformNames(game.Platforms),
			Rating:         normalizeRating(game.Rating),
			RuntimeMinutes: 0,
			ArtworkKey:     fmt.Sprintf("igdb-%d", game.ID),
		})
	}

	return items, nil
}

func (c *Client) token(ctx context.Context) (string, error) {
	c.mu.Lock()
	if c.accessToken != "" && time.Now().Before(c.accessTokenExp.Add(-1*time.Minute)) {
		token := c.accessToken
		c.mu.Unlock()
		return token, nil
	}
	c.mu.Unlock()

	form := url.Values{}
	form.Set("client_id", c.cfg.ClientID)
	form.Set("client_secret", c.cfg.ClientSecret)
	form.Set("grant_type", "client_credentials")

	request, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		c.cfg.TokenURL,
		strings.NewReader(form.Encode()),
	)
	if err != nil {
		return "", err
	}

	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	response, err := c.httpClient.Do(request)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()

	if response.StatusCode >= http.StatusBadRequest {
		message, _ := io.ReadAll(io.LimitReader(response.Body, 2048))
		return "", fmt.Errorf("igdb token request failed: %s", strings.TrimSpace(string(message)))
	}

	var payload tokenResponse
	if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
		return "", err
	}

	if payload.AccessToken == "" {
		return "", errors.New("igdb token response missing access token")
	}

	c.mu.Lock()
	c.accessToken = payload.AccessToken
	c.accessTokenExp = time.Now().Add(time.Duration(payload.ExpiresIn) * time.Second)
	token := c.accessToken
	c.mu.Unlock()

	return token, nil
}

func unixDate(value int64) string {
	if value <= 0 {
		return ""
	}

	return time.Unix(value, 0).UTC().Format("2006-01-02")
}

func normalizeRating(value float64) float64 {
	if value > 10 {
		return value / 10
	}

	return value
}

func genreNames(values []struct {
	Name string `json:"name"`
}) []string {
	if len(values) == 0 {
		return []string{}
	}

	names := make([]string, 0, len(values))
	for _, value := range values {
		name := strings.TrimSpace(value.Name)
		if name != "" {
			names = append(names, name)
		}
	}
	return names
}

func platformNames(values []struct {
	Name string `json:"name"`
}) []string {
	if len(values) == 0 {
		return []string{}
	}

	names := make([]string, 0, len(values))
	for _, value := range values {
		name := strings.TrimSpace(value.Name)
		if name != "" {
			names = append(names, name)
		}
	}
	return names
}
