package allanime

import (
	"context"
	"fmt"
	"mal/pkg"
	netutil "mal/pkg/net"
	"strconv"
	"strings"
)

const searchQuery = `query(
  $search: SearchInput
  $translationType: VaildTranslationTypeEnumType
  $limit: Int = 40
  $page: Int = 1
  $countryOrigin: VaildCountryOriginEnumType = ALL
) {
  shows(
    search: $search
    limit: $limit
    page: $page
    translationType: $translationType
    countryOrigin: $countryOrigin
  ) {
    edges {
      _id
      malId
      name
    }
  }
}`

type searchResult struct {
	ID    string
	MalID string
	Name  string
}

func (c *AllAnimeProvider) Search(ctx context.Context, query string, mode string) ([]searchResult, error) {
	type searchData struct {
		Shows struct {
			Edges []struct {
				ID    string `json:"_id"`
				MalID string `json:"malId"`
				Name  string `json:"name"`
			} `json:"edges"`
		} `json:"shows"`
	}

	type searchInput struct {
		AllowAdult   bool   `json:"allowAdult"`
		AllowUnknown bool   `json:"allowUnknown"`
		Query        string `json:"query"`
	}

	type searchVariables struct {
		Search          searchInput `json:"search"`
		TranslationType string      `json:"translationType"`
	}

	vars := searchVariables{
		Search: searchInput{
			AllowAdult:   false,
			AllowUnknown: false,
			Query:        query,
		},
		TranslationType: mode,
	}

	data, err := graphql.Post[searchData](ctx, c.httpClient, allAnimeBaseURL+"/api", searchQuery, vars, graphql.PostOptions{
		Headers: map[string]string{
			"Referer":    allAnimeReferer,
			"User-Agent": defaultUserAgent,
		},
		BodyMax: netutil.MiB2,
	})
	if err != nil {
		return nil, err
	}

	out := make([]searchResult, 0, len(data.Shows.Edges))
	for _, edge := range data.Shows.Edges {
		id := edge.ID
		malID := edge.MalID
		name := edge.Name
		if unquoted, err := strconv.Unquote("\"" + name + "\""); err == nil {
			name = unquoted
		}
		name = strings.TrimSpace(name)

		if id == "" {
			continue
		}

		out = append(out, searchResult{ID: id, MalID: malID, Name: name})
	}

	return out, nil
}

func (c *AllAnimeProvider) resolveShowIDWithFallback(ctx context.Context, animeID int, titleCandidates []string, mode string) string {
	targetMalIDStr := strconv.Itoa(animeID)
	firstAvailableShowID := ""

	for _, title := range titleCandidates {
		searchResults, err := c.Search(ctx, title, mode)
		if err != nil || len(searchResults) == 0 {
			continue
		}
		if showID := exactMatchShowID(searchResults, targetMalIDStr); showID != "" {
			return showID
		}
		if firstAvailableShowID == "" {
			firstAvailableShowID = searchResults[0].ID
		}
	}

	return firstAvailableShowID
}

func exactMatchShowID(searchResults []searchResult, targetMalID string) string {
	for _, res := range searchResults {
		if res.MalID == targetMalID {
			return res.ID
		}
	}

	return ""
}

func (c *AllAnimeProvider) ResolveEpisodeProviderID(ctx context.Context, animeID int, titleCandidates []string) (string, error) {
	for _, mode := range []string{"sub", "dub"} {
		showID, err := c.resolveShowIDStrict(ctx, animeID, titleCandidates, mode)
		if err == nil {
			return showID, nil
		}
	}
	return "", fmt.Errorf("allanime: no exact mal id match for %d", animeID)
}

func (c *AllAnimeProvider) resolveShowIDStrict(ctx context.Context, animeID int, titleCandidates []string, mode string) (string, error) {
	targetMalIDStr := strconv.Itoa(animeID)
	for _, title := range titleCandidates {
		searchResults, err := c.Search(ctx, title, mode)
		if err != nil {
			continue
		}
		for _, res := range searchResults {
			if res.MalID == targetMalIDStr {
				return res.ID, nil
			}
		}
	}
	return "", fmt.Errorf("allanime: no exact mal id match for %d in %s search", animeID, mode)
}
