package curseforge

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// addonSlugRequest is sent to the CurseProxy GraphQL api to get the id from a slug
type addonSlugRequest struct {
	Query     string `json:"query"`
	Variables struct {
		Slug string `json:"slug"`
	} `json:"variables"`
}

// addonSlugResponse is received from the CurseProxy GraphQL api to get the id from a slug
type addonSlugResponse struct {
	Data struct {
		Addons []struct {
			ID int `json:"id"`
		} `json:"addons"`
	} `json:"data"`
	Exception  string   `json:"exception"`
	Message    string   `json:"message"`
	Stacktrace []string `json:"stacktrace"`
}

// Most of this is shamelessly copied from my previous attempt at modpack management:
// https://github.com/comp500/modpack-editor/blob/master/query.go
func modIDFromSlug(slug string) (int, error) {
	request := addonSlugRequest{
		Query: `
		query getIDFromSlug($slug: String) {
			{
				addons(slug: $slug) {
					id
				}
			}
		}
		`,
	}
	request.Variables.Slug = slug

	// Uses the curse.nikky.moe GraphQL api
	var response addonSlugResponse
	client := &http.Client{}

	requestBytes, err := json.Marshal(request)
	if err != nil {
		return 0, err
	}

	req, err := http.NewRequest("POST", "https://curse.nikky.moe/graphql", bytes.NewBuffer(requestBytes))
	if err != nil {
		return 0, err
	}

	// TODO: make this configurable application-wide
	req.Header.Set("User-Agent", "comp500/packwiz client")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return 0, err
	}

	err = json.NewDecoder(resp.Body).Decode(&response)
	if err != nil && err != io.EOF {
		return 0, err
	}

	if len(response.Exception) > 0 || len(response.Message) > 0 {
		return 0, fmt.Errorf("Error requesting id for slug: %s", response.Message)
	}

	if len(response.Data.Addons) < 1 {
		return 0, errors.New("Addon not found")
	}

	return response.Data.Addons[0].ID, nil
}

const (
	fileTypeRelease int = iota + 1
	fileTypeBeta
	fileTypeAlpha
)

const (
	dependencyTypeEmbedded int = iota + 1
	dependencyTypeOptional
	dependencyTypeRequired
	dependencyTypeTool
	dependencyTypeIncompatible
	dependencyTypeInclude
)

// modInfo is a subset of the deserialised JSON response from the Curse API for mods (addons)
type modInfo struct {
	Name                   string        `json:"name"`
	Slug                   string        `json:"slug"`
	ID                     int           `json:"id"`
	LatestFiles            []modFileInfo `json:"latestFiles"`
	GameVersionLatestFiles []struct {
		// TODO: check how twitch launcher chooses which one to use, when you are on beta/alpha channel?!
		// or does it not have the concept of release channels?!
		GameVersion string `json:"gameVersion"`
		ID          int    `json:"projectFileId"`
		Name        string `json:"projectFileName"`
		FileType    int    `json:"fileType"`
	} `json:"gameVersionLatestFiles"`
}

func getModInfo(modID int) (modInfo, error) {
	var infoRes modInfo
	client := &http.Client{}

	idStr := strconv.Itoa(modID)

	req, err := http.NewRequest("GET", "https://addons-ecs.forgesvc.net/api/v2/addon/"+idStr, nil)
	if err != nil {
		return modInfo{}, err
	}

	// TODO: make this configurable application-wide
	req.Header.Set("User-Agent", "comp500/packwiz client")
	req.Header.Set("Accept", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return modInfo{}, err
	}

	err = json.NewDecoder(resp.Body).Decode(&infoRes)
	if err != nil && err != io.EOF {
		return modInfo{}, err
	}

	if infoRes.ID != modID {
		return modInfo{}, fmt.Errorf("Unexpected addon ID in CurseForge response: %d/%d", modID, infoRes.ID)
	}

	return infoRes, nil
}

func getModInfoMultiple(modIDs []int) ([]modInfo, error) {
	var infoRes []modInfo
	client := &http.Client{}

	modIDsData, err := json.Marshal(modIDs)
	if err != nil {
		return []modInfo{}, err
	}

	req, err := http.NewRequest("POST", "https://addons-ecs.forgesvc.net/api/v2/addon/", bytes.NewBuffer(modIDsData))
	if err != nil {
		return []modInfo{}, err
	}

	// TODO: make this configurable application-wide
	req.Header.Set("User-Agent", "comp500/packwiz client")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return []modInfo{}, err
	}

	err = json.NewDecoder(resp.Body).Decode(&infoRes)
	if err != nil && err != io.EOF {
		return []modInfo{}, err
	}

	return infoRes, nil
}

const cfDateFormatString = "2006-01-02T15:04:05.999"

type cfDateFormat struct {
	time.Time
}

// Curse switched to proper RFC3339, but previously downloaded metadata still uses the old format :(
func (f *cfDateFormat) UnmarshalJSON(input []byte) error {
	trimmed := strings.Trim(string(input), `"`)
	timeValue, err := time.Parse(time.RFC3339Nano, trimmed)
	if err != nil {
		timeValue, err = time.Parse(cfDateFormatString, trimmed)
		if err != nil {
			return err
		}
	}

	f.Time = timeValue
	return nil
}

// modFileInfo is a subset of the deserialised JSON response from the Curse API for mod files
type modFileInfo struct {
	ID           int          `json:"id"`
	FileName     string       `json:"fileName"`
	FriendlyName string       `json:"displayName"`
	Date         cfDateFormat `json:"fileDate"`
	Length       int          `json:"fileLength"`
	FileType     int          `json:"releaseType"`
	// fileStatus? means latest/preferred?
	DownloadURL  string   `json:"downloadUrl"`
	GameVersions []string `json:"gameVersion"`
	Fingerprint  int      `json:"packageFingerprint"`
	Dependencies []struct {
		ModID int `json:"addonId"`
		Type  int `json:"type"`
	} `json:"dependencies"`
}

func getFileInfo(modID int, fileID int) (modFileInfo, error) {
	var infoRes modFileInfo
	client := &http.Client{}

	modIDStr := strconv.Itoa(modID)
	fileIDStr := strconv.Itoa(fileID)

	req, err := http.NewRequest("GET", "https://addons-ecs.forgesvc.net/api/v2/addon/"+modIDStr+"/file/"+fileIDStr, nil)
	if err != nil {
		return modFileInfo{}, err
	}

	// TODO: make this configurable application-wide
	req.Header.Set("User-Agent", "comp500/packwiz client")
	req.Header.Set("Accept", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return modFileInfo{}, err
	}

	err = json.NewDecoder(resp.Body).Decode(&infoRes)
	if err != nil && err != io.EOF {
		return modFileInfo{}, err
	}

	if infoRes.ID != fileID {
		return modFileInfo{}, fmt.Errorf("Unexpected file ID in CurseForge response: %d/%d", modID, infoRes.ID)
	}

	return infoRes, nil
}

// TODO: pass gameVersion?
func getSearch(searchText string, gameVersion string) ([]modInfo, error) {
	var infoRes []modInfo
	client := &http.Client{}

	textEscaped := url.QueryEscape(searchText)
	var reqURL string
	if len(gameVersion) > 0 {
		reqURL = "https://addons-ecs.forgesvc.net/api/v2/addon/search?gameId=432&pageSize=10&categoryId=0&sectionId=6&searchFilter=" + textEscaped + "&gameVersion=" + gameVersion
	} else {
		reqURL = "https://addons-ecs.forgesvc.net/api/v2/addon/search?gameId=432&pageSize=10&categoryId=0&sectionId=6&searchFilter=" + textEscaped
	}

	req, err := http.NewRequest("GET", reqURL, nil)
	if err != nil {
		return []modInfo{}, err
	}

	// TODO: make this configurable application-wide
	req.Header.Set("User-Agent", "comp500/packwiz client")
	req.Header.Set("Accept", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return []modInfo{}, err
	}

	err = json.NewDecoder(resp.Body).Decode(&infoRes)
	if err != nil && err != io.EOF {
		return []modInfo{}, err
	}

	return infoRes, nil
}