package extension_playground

import (
	"bytes"
	"context"
	"fmt"
	"runtime"
	"seanime/internal/api/anilist"
	"seanime/internal/api/metadata"
	"seanime/internal/extension"
	hibikemanga "seanime/internal/extension/hibike/manga"
	hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
	hibiketorrent "seanime/internal/extension/hibike/torrent"
	"seanime/internal/extension_repo"
	goja_runtime "seanime/internal/goja/goja_runtime"
	"seanime/internal/manga"
	"seanime/internal/onlinestream"
	"seanime/internal/platforms/platform"
	"seanime/internal/util"
	"seanime/internal/util/result"
	"strconv"
	"strings"
	"time"

	"github.com/davecgh/go-spew/spew"
	"github.com/goccy/go-json"
	"github.com/rs/zerolog"
)

type (
	PlaygroundRepository struct {
		logger             *zerolog.Logger
		platform           platform.Platform
		baseAnimeCache     *result.Cache[int, *anilist.BaseAnime]
		baseMangaCache     *result.Cache[int, *anilist.BaseManga]
		metadataProvider   metadata.Provider
		gojaRuntimeManager *goja_runtime.Manager
	}

	RunPlaygroundCodeResponse struct {
		Logs  string `json:"logs"`
		Value string `json:"value"`
	}

	RunPlaygroundCodeParams struct {
		Type     extension.Type         `json:"type"`
		Language extension.Language     `json:"language"`
		Code     string                 `json:"code"`
		Inputs   map[string]interface{} `json:"inputs"`
		Function string                 `json:"function"`
	}
)

func NewPlaygroundRepository(logger *zerolog.Logger, platform platform.Platform, metadataProvider metadata.Provider) *PlaygroundRepository {
	return &PlaygroundRepository{
		logger:             logger,
		platform:           platform,
		metadataProvider:   metadataProvider,
		baseAnimeCache:     result.NewCache[int, *anilist.BaseAnime](),
		baseMangaCache:     result.NewCache[int, *anilist.BaseManga](),
		gojaRuntimeManager: goja_runtime.NewManager(logger),
	}
}

func (r *PlaygroundRepository) RunPlaygroundCode(params *RunPlaygroundCodeParams) (resp *RunPlaygroundCodeResponse, err error) {
	defer util.HandlePanicInModuleWithError("extension_playground/RunPlaygroundCode", &err)

	if params == nil {
		return nil, fmt.Errorf("no parameters provided")
	}

	ext := &extension.Extension{
		ID:          "playground-extension",
		Name:        "Playground",
		Version:     "0.0.0",
		ManifestURI: "",
		Language:    params.Language,
		Type:        params.Type,
		Description: "",
		Author:      "",
		Icon:        "",
		Website:     "",
		Payload:     params.Code,
	}

	r.logger.Debug().Msgf("playground: Inputs: %s", strings.ReplaceAll(spew.Sprint(params.Inputs), "\n", ""))

	switch params.Type {
	case extension.TypeMangaProvider:
		return r.runPlaygroundCodeMangaProvider(ext, params)
	case extension.TypeOnlinestreamProvider:
		return r.runPlaygroundCodeOnlinestreamProvider(ext, params)
	case extension.TypeAnimeTorrentProvider:
		return r.runPlaygroundCodeAnimeTorrentProvider(ext, params)
	default:
	}

	runtime.GC()

	return nil, fmt.Errorf("invalid extension type")
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

type PlaygroundDebugLogger struct {
	logger *zerolog.Logger
	buff   *bytes.Buffer
}

func (r *PlaygroundRepository) newPlaygroundDebugLogger() *PlaygroundDebugLogger {
	buff := bytes.NewBuffer(nil)
	consoleWritier := zerolog.ConsoleWriter{
		Out:           buff,
		TimeFormat:    time.DateTime,
		FormatMessage: util.ZerologFormatMessageSimple,
		FormatLevel:   util.ZerologFormatLevelSimple,
		NoColor:       true, // Needed to prevent color codes from being written to the file
	}

	logger := zerolog.New(consoleWritier).With().Timestamp().Logger()

	return &PlaygroundDebugLogger{
		logger: &logger,
		buff:   buff,
	}
}

func newPlaygroundResponse(playgroundLogger *PlaygroundDebugLogger, value interface{}) *RunPlaygroundCodeResponse {
	v := ""

	switch value.(type) {
	case error:
		v = fmt.Sprintf("ERROR: %+v", value)
	case string:
		v = value.(string)
	default:
		// Pretty print the value to json
		prettyJSON, err := json.MarshalIndent(value, "", "  ")
		if err != nil {
			v = fmt.Sprintf("ERROR: Failed to marshal value to JSON: %+v", err)
		} else {
			v = string(prettyJSON)
		}
	}

	logs := playgroundLogger.buff.String()

	return &RunPlaygroundCodeResponse{
		Logs:  logs,
		Value: v,
	}
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

func (r *PlaygroundRepository) getAnime(mediaId int) (anime *anilist.BaseAnime, am *metadata.AnimeMetadata, err error) {
	var ok bool
	anime, ok = r.baseAnimeCache.Get(mediaId)
	if !ok {
		anime, err = r.platform.GetAnime(context.Background(), mediaId)
		if err != nil {
			return nil, nil, err
		}
		r.baseAnimeCache.SetT(mediaId, anime, 24*time.Hour)
	}

	am, _ = r.metadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, mediaId)
	return anime, am, nil
}

func (r *PlaygroundRepository) getManga(mediaId int) (manga *anilist.BaseManga, err error) {
	var ok bool
	manga, ok = r.baseMangaCache.Get(mediaId)
	if !ok {
		manga, err = r.platform.GetManga(context.Background(), mediaId)
		if err != nil {
			return nil, err
		}
		r.baseMangaCache.SetT(mediaId, manga, 24*time.Hour)
	}
	return
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

func (r *PlaygroundRepository) runPlaygroundCodeAnimeTorrentProvider(ext *extension.Extension, params *RunPlaygroundCodeParams) (resp *RunPlaygroundCodeResponse, err error) {

	playgroundLogger := r.newPlaygroundDebugLogger()

	// Inputs
	// - mediaId int
	// - options struct

	mediaId, ok := params.Inputs["mediaId"].(float64)
	if !ok || mediaId <= 0 {
		return nil, fmt.Errorf("invalid mediaId")
	}

	// Fetch the anime
	anime, animeMetadata, err := r.getAnime(int(mediaId))
	if err != nil {
		return nil, err
	}

	queryMedia := hibiketorrent.Media{
		ID:                   anime.GetID(),
		IDMal:                anime.GetIDMal(),
		Status:               string(*anime.GetStatus()),
		Format:               string(*anime.GetFormat()),
		EnglishTitle:         anime.GetTitle().GetEnglish(),
		RomajiTitle:          anime.GetRomajiTitleSafe(),
		EpisodeCount:         anime.GetTotalEpisodeCount(),
		AbsoluteSeasonOffset: 0,
		Synonyms:             anime.GetSynonymsContainingSeason(),
		IsAdult:              *anime.GetIsAdult(),
		StartDate: &hibiketorrent.FuzzyDate{
			Year:  *anime.GetStartDate().GetYear(),
			Month: anime.GetStartDate().GetMonth(),
			Day:   anime.GetStartDate().GetDay(),
		},
	}

	switch params.Language {
	case extension.LanguageGo:
	//...
	case extension.LanguageJavascript, extension.LanguageTypescript:
		_, provider, err := extension_repo.NewGojaAnimeTorrentProvider(ext, params.Language, playgroundLogger.logger, r.gojaRuntimeManager)
		if err != nil {
			return newPlaygroundResponse(playgroundLogger, err), nil
		}
		defer r.gojaRuntimeManager.DeletePluginPool(ext.ID)

		// Run the code
		switch params.Function {
		case "search":
			res, err := provider.Search(hibiketorrent.AnimeSearchOptions{
				Media: queryMedia,
				Query: params.Inputs["query"].(string),
			})
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil
		case "smartSearch":
			type p struct {
				Query         string `json:"query"`
				Batch         bool   `json:"batch"`
				EpisodeNumber int    `json:"episodeNumber"`
				Resolution    string `json:"resolution"`
				BestReleases  bool   `json:"bestReleases"`
			}
			m, _ := json.Marshal(params.Inputs["options"])
			var options p
			_ = json.Unmarshal(m, &options)

			anidbAID := 0
			anidbEID := 0

			// Get the AniDB Anime ID and Episode ID
			if animeMetadata != nil {
				// Override absolute offset value of queryMedia
				queryMedia.AbsoluteSeasonOffset = animeMetadata.GetOffset()

				if animeMetadata.GetMappings() != nil {

					anidbAID = animeMetadata.GetMappings().AnidbId
					// Find Animap Episode based on inputted episode number
					anizipEpisode, found := animeMetadata.FindEpisode(strconv.Itoa(options.EpisodeNumber))
					if found {
						anidbEID = anizipEpisode.AnidbEid
					}
				}
			}

			res, err := provider.SmartSearch(hibiketorrent.AnimeSmartSearchOptions{
				Media:         queryMedia,
				Query:         options.Query,
				Batch:         options.Batch,
				EpisodeNumber: options.EpisodeNumber,
				Resolution:    options.Resolution,
				BestReleases:  options.BestReleases,
				AnidbAID:      anidbAID,
				AnidbEID:      anidbEID,
			})
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil
		case "getTorrentInfoHash":
			var torrent hibiketorrent.AnimeTorrent
			_ = json.Unmarshal([]byte(params.Inputs["torrent"].(string)), &torrent)

			res, err := provider.GetTorrentInfoHash(&torrent)
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil
		case "getTorrentMagnetLink":
			var torrent hibiketorrent.AnimeTorrent
			_ = json.Unmarshal([]byte(params.Inputs["torrent"].(string)), &torrent)

			res, err := provider.GetTorrentMagnetLink(&torrent)
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil
		case "getLatest":
			res, err := provider.GetLatest()
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil
		case "getSettings":
			res := provider.GetSettings()
			return newPlaygroundResponse(playgroundLogger, res), nil
		}
	}

	return nil, fmt.Errorf("unknown call")
}

func (r *PlaygroundRepository) runPlaygroundCodeMangaProvider(ext *extension.Extension, params *RunPlaygroundCodeParams) (resp *RunPlaygroundCodeResponse, err error) {

	playgroundLogger := r.newPlaygroundDebugLogger()

	mediaId, ok := params.Inputs["mediaId"].(float64)
	if !ok || mediaId <= 0 {
		return nil, fmt.Errorf("invalid mediaId")
	}

	media, err := r.getManga(int(mediaId))
	if err != nil {
		return nil, err
	}

	titles := media.GetAllTitles()

	switch params.Language {
	case extension.LanguageGo:
	//...
	case extension.LanguageJavascript, extension.LanguageTypescript:
		_, provider, err := extension_repo.NewGojaMangaProvider(ext, params.Language, playgroundLogger.logger, r.gojaRuntimeManager)
		if err != nil {
			return newPlaygroundResponse(playgroundLogger, err), nil
		}
		defer r.gojaRuntimeManager.DeletePluginPool(ext.ID)

		// Run the code
		switch params.Function {
		case "search":
			// Search
			y := 0
			if media.GetStartDate().GetYear() != nil {
				y = *media.GetStartDate().GetYear()
			}

			ret := make([]*hibikemanga.SearchResult, 0)
			for _, title := range titles {
				res, err := provider.Search(hibikemanga.SearchOptions{
					Query: *title,
					Year:  y,
				})
				if err != nil {
					playgroundLogger.logger.Error().Err(err).Msgf("playground: Search failed for title \"%s\"", *title)
				}
				manga.HydrateSearchResultSearchRating(res, title)
				ret = append(ret, res...)
			}

			var selected *hibikemanga.SearchResult
			if len(ret) > 0 {
				selected = manga.GetBestSearchResult(ret)
			}

			return newPlaygroundResponse(playgroundLogger, selected), nil

		case "findChapters":
			res, err := provider.FindChapters(params.Inputs["id"].(string))
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil

		case "findChapterPages":
			res, err := provider.FindChapterPages(params.Inputs["id"].(string))
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil
		}
	}

	return nil, fmt.Errorf("unknown call")
}

func (r *PlaygroundRepository) runPlaygroundCodeOnlinestreamProvider(ext *extension.Extension, params *RunPlaygroundCodeParams) (resp *RunPlaygroundCodeResponse, err error) {

	playgroundLogger := r.newPlaygroundDebugLogger()

	mediaId, ok := params.Inputs["mediaId"].(float64)
	if !ok || mediaId <= 0 {
		return nil, fmt.Errorf("invalid mediaId")
	}

	// Fetch the anime
	anime, _, err := r.getAnime(int(mediaId))
	if err != nil {
		return nil, err
	}

	titles := anime.GetAllTitles()

	queryMedia := hibikeonlinestream.Media{
		ID:           anime.GetID(),
		IDMal:        anime.GetIDMal(),
		Status:       string(*anime.GetStatus()),
		Format:       string(*anime.GetFormat()),
		EnglishTitle: anime.GetTitle().GetEnglish(),
		RomajiTitle:  anime.GetRomajiTitleSafe(),
		EpisodeCount: anime.GetTotalEpisodeCount(),
		Synonyms:     anime.GetSynonymsContainingSeason(),
		IsAdult:      *anime.GetIsAdult(),
		StartDate: &hibikeonlinestream.FuzzyDate{
			Year:  *anime.GetStartDate().GetYear(),
			Month: anime.GetStartDate().GetMonth(),
			Day:   anime.GetStartDate().GetDay(),
		},
	}

	switch params.Language {
	case extension.LanguageGo:
	//...
	case extension.LanguageJavascript, extension.LanguageTypescript:
		_, provider, err := extension_repo.NewGojaOnlinestreamProvider(ext, params.Language, playgroundLogger.logger, r.gojaRuntimeManager)
		if err != nil {
			return newPlaygroundResponse(playgroundLogger, err), nil
		}
		defer r.gojaRuntimeManager.DeletePluginPool(ext.ID)

		// Run the code
		switch params.Function {
		case "search":
			// Search - params: dub: boolean
			ret := make([]*hibikeonlinestream.SearchResult, 0)
			for _, title := range titles {
				res, err := provider.Search(hibikeonlinestream.SearchOptions{
					Media: queryMedia,
					Query: *title,
					Dub:   params.Inputs["dub"].(bool),
					Year:  anime.GetStartYearSafe(),
				})
				if err != nil {
					playgroundLogger.logger.Error().Err(err).Msgf("playground: Search failed for title \"%s\"", *title)
				}
				ret = append(ret, res...)
			}

			if len(ret) == 0 {
				return newPlaygroundResponse(playgroundLogger, onlinestream.ErrNoAnimeFound), nil
			}

			bestRes, found := onlinestream.GetBestSearchResult(ret, titles)
			if !found {
				return newPlaygroundResponse(playgroundLogger, onlinestream.ErrNoAnimeFound), nil
			}

			return newPlaygroundResponse(playgroundLogger, bestRes), nil

		case "findEpisodes":
			// FindEpisodes - params: id: string
			res, err := provider.FindEpisodes(params.Inputs["id"].(string))
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil

		case "findEpisodeServer":
			// FindEpisodeServer - params: episode: EpisodeDetails, server: string
			var episode hibikeonlinestream.EpisodeDetails
			_ = json.Unmarshal([]byte(params.Inputs["episode"].(string)), &episode)

			res, err := provider.FindEpisodeServer(&episode, params.Inputs["server"].(string))
			if err != nil {
				return newPlaygroundResponse(playgroundLogger, err), nil
			}
			return newPlaygroundResponse(playgroundLogger, res), nil
		}
	}

	return nil, fmt.Errorf("unknown call")
}
