package aoc

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"time"
)

type AoC struct {
	session string
	boardId string
	boards  map[int]*Leaderboard
}

func NewAoC(boardId, session string) (*AoC, error) {
	if boardId == "" {
		return nil, errors.New("Board ID is required")
	}
	if session == "" {
		return nil, errors.New("Session key is required")
	}
	return &AoC{
		session: session,
		boardId: boardId,
		boards:  make(map[int]*Leaderboard),
	}, nil
}

func (a *AoC) GetCachedLeaderboard(year int) (*Leaderboard, error) {
	if year < 2015 || year > time.Now().Year() {
		return nil, errors.New("Invalid Year")
	}
	if board, ok := a.boards[year]; ok {
		return board, nil
	}
	return nil, errors.New(fmt.Sprintf("Leaderboard (%d) is not cached", year))
}

func (a *AoC) GetLeaderboard(year int) (*Leaderboard, error) {
	if year < 2015 || year > time.Now().Year() {
		return nil, errors.New("Invalid Year")
	}
	if board, ok := a.boards[year]; ok {
		if time.Since(board.LastFetch) < (time.Minute * 15) {
			return board, nil
		}
	}
	var err error
	a.boards[year], err = a.fetchLeaderboard(year)
	if err != nil {
		if err.Error() == "invalid character '<' looking for beginning of value" {
			return nil, errors.New("Invalid Session Cookie")
		}
		return nil, err
	}
	return a.boards[year], nil
}

func (a *AoC) fetchLeaderboard(year int) (*Leaderboard, error) {
	var err error
	var req *http.Request
	var resp *http.Response
	var body []byte
	leaderboard := new(Leaderboard)
	leaderboard.Members = make(map[string]Member)

	client := &http.Client{}
	boardString := fmt.Sprintf("https://adventofcode.com/%d/leaderboard/private/view/%s.json", year, a.boardId)
	req, err = http.NewRequest("GET", boardString, nil)
	req.Header.Add("Cookie", "session="+a.session)
	resp, err = client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	strBody := string(body)
	//strBody = strings.ReplaceAll(strBody, "\"last_star_ts\":\"0\"", "\"last_star_ts\":0")
	err = json.Unmarshal([]byte(strBody), &leaderboard)
	if err != nil {
		fmt.Println("Error parsing board")
		fmt.Println(err)
		return nil, err
	}
	for k, mbr := range leaderboard.Members {
		starTs, err := strconv.ParseInt(mbr.RawStarTs, 10, 64)
		if err != nil {
			continue
		}
		mbr.LastStarTs = time.Unix(starTs, 0)
		leaderboard.Members[k] = mbr
	}
	leaderboard.LastFetch = time.Now()
	a.boards[year] = leaderboard
	return leaderboard, err
}

type Leaderboard struct {
	OwnerID   string            `json:"owner_id"`
	Event     string            `json:"event"`
	Members   map[string]Member `json:"members"`
	LastFetch time.Time
}

type Member struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Stars       int    `json:"stars"`
	RawStarTs   string `json:"last_star_ts"`
	LocalScore  int    `json:"local_score"`
	GlobalScore int    `json:"global_score"`
	LastStarTs  time.Time
	SlackID     string
}

type ByLocalScore []Member

func (a ByLocalScore) Len() int           { return len(a) }
func (a ByLocalScore) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByLocalScore) Less(i, j int) bool { return a[i].LocalScore < a[j].LocalScore }

type ByStars []Member

func (a ByStars) Len() int           { return len(a) }
func (a ByStars) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByStars) Less(i, j int) bool { return a[i].Stars < a[j].Stars }

type ByStarsThenScore []Member

func (a ByStarsThenScore) Len() int      { return len(a) }
func (a ByStarsThenScore) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByStarsThenScore) Less(i, j int) bool {
	if a[i].Stars == a[j].Stars {
		return a[i].LocalScore < a[j].LocalScore
	}
	return a[i].Stars < a[j].Stars
}

func (m *Member) UnmarshalJSON(data []byte) error {

	var v map[string]interface{}
	var err error

	if err = json.Unmarshal(data, &v); err != nil {
		return err
	}
	m.ID = v["id"].(string)
	m.Name = v["name"].(string)
	m.Stars = int(v["stars"].(float64))

	switch v["last_star_ts"].(type) {
	case float64:
		m.RawStarTs = fmt.Sprintf("%d", int(v["last_star_ts"].(float64)))
	case int:
		m.RawStarTs = strconv.Itoa(v["last_star_ts"].(int))
	case string:
		m.RawStarTs = v["last_star_ts"].(string)
	default:
		m.RawStarTs = "0"
	}
	starTs, err := strconv.ParseInt(m.RawStarTs, 10, 64)
	if err != nil {
		starTs = 0
	}
	m.LastStarTs = time.Unix(starTs, 0)

	return nil
}
