package auth

import (
	"encoding/json"

	"github.com/nicklaw5/helix/v2"

	"context"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"time"
)

// TODO; Handle scopes properly

type Auth struct {
	HaveAuth bool
	client   *helix.Client
	context  *context.Context
}

var authTimeout time.Duration = time.Second * 60

func New(ctx *context.Context, c *helix.Client, s []string) (*Auth, error) {
	var auth *Auth = &Auth{context: ctx, client: c}

	// See about getting a token
	c.SetRedirectURI("http://localhost:3000")

	// See if there are any stored credentials.
	creds, err := getUserToken()
	if err != nil {
		// Caller needs to get followup to get credentials via StartAuth()
		return auth, nil
	}

	// Validate token in case what we've got on file is still good.
	validate := func() error {
		valid, resp, err := c.ValidateToken(creds.AccessToken)
		if valid {
			return nil
		}

		if err != nil {
			log.Printf("Validation error: %v\n", err)
		}

		_ = resp
		return err
	}

	// Attempt to refresh the token if possible.
	refresh := func() (*helix.AccessCredentials, error) {
		resp, err := c.RefreshUserAccessToken(creds.RefreshToken)
		if err != nil {
			log.Printf("Error refreshing token: %v\n", err)
			return nil, err
		}

		return &resp.Data, nil
	}

	// FIXME: This should return an error.
	if err := validate(); err != nil {
		return auth, err
	}

	refreshedToken, err := refresh()
	if err != nil {
		return auth, err
	}

	// XXX: Maybe wrap this in a function.
	c.SetUserAccessToken(refreshedToken.AccessToken)
	c.SetRefreshToken(refreshedToken.RefreshToken)
	storeUserToken(refreshedToken)
	auth.HaveAuth = true

	return auth, nil
}

// Returns path to a token file.  Creates it if it does nt exist.
func tokenFilePath() (string, error) {
	storePath, err := os.UserConfigDir()
	if err != nil || storePath == "" {
		// Fallback to HOME-based path if UserConfigDir fails or returns empty
		home := os.Getenv("HOME")
		if home == "" {
			log.Printf("UserConfigDir failed and HOME not set: %v", err)
			return "", os.ErrNotExist
		}

		// Use platform-appropriate config directory
		if runtime.GOOS == "darwin" {
			storePath = filepath.Join(home, "Library", "Application Support")
		} else {
			storePath = filepath.Join(home, ".config")
		}
	}

	storePath = filepath.Join(storePath, "tremble")
	err = os.MkdirAll(storePath, 0755)
	if err != nil {
		log.Printf("Error creating %s: %v\n", storePath, err)
		return "", err
	}

	return filepath.Join(storePath, "token.json"), nil
}

// Only reporting here for now since it's probably not worth trying to recover
// from an error here.
func storeUserToken(token *helix.AccessCredentials) {
	tf, err := tokenFilePath()
	if err != nil {
		return
	}

	b, err := json.Marshal(token)
	if err != nil {
		log.Printf("Couldn't marshal %+v: %v\n", token, err)
		return
	}

	err = os.WriteFile(tf, b, 0644)
	if err != nil {
		log.Printf("Error writing token: %v\n", err)
		return
	}
}

func getUserToken() (creds helix.AccessCredentials, err error) {
	tf, err := tokenFilePath()
	if err != nil {
		return
	}

	b, err := os.ReadFile(tf)
	if err != nil {

		if err != os.ErrNotExist {
			log.Printf("Couldn't read token file: %v\n", err)
		}
		return
	}

	if err = json.Unmarshal(b, &creds); err != nil {
		log.Printf("Error unmarshalling token: %v\n", err)
		return
	}

	return
}

func (a *Auth) updateCode(code string) error {
	resp, err := a.client.RequestUserAccessToken(code)
	if err != nil {
		log.Printf("error requesting user access token: %v\n", err)
		return err
	}

	log.Printf("%+v\n", resp.Data)

	a.client.SetUserAccessToken(resp.Data.AccessToken)
	storeUserToken(&resp.Data)
	return nil
}

func (a *Auth) StartAuth(userId string, s []string) (chan int, error) {
	// Need to get authorization
	authUrl := a.client.GetAuthorizationURL(&helix.AuthorizationURLParams{
		ResponseType: "code",
		Scopes:       s,
		State:        userId,
		ForceVerify:  false,
	})

	osType := runtime.GOOS

	// Open browser and do the auth.  Let the auth listener take care of the
	// rest.
	switch osType {
	case "linux":
		cmd := exec.Command("xdg-open", authUrl)
		if err := cmd.Run(); err != nil {
			return nil, err
		}
	case "windows":
		cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", authUrl)
		if err := cmd.Run(); err != nil {
			return nil, err
		}
	case "darwin":
		cmd := exec.Command("open", authUrl)
		if err := cmd.Run(); err != nil {
			return nil, err
		}
	default:
		log.Printf("Please open a browser and go to: %s\n", authUrl)
	}

	notify := make(chan int)

	a.waitForAuth(notify)

	return notify, nil
}

func (a *Auth) waitForAuth(notify chan int) {
	authCtx, cancel := context.WithTimeout(*a.context, authTimeout)
	srv := &http.Server{Addr: ":3000"}

	var code string = ""

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		select {
		case <-authCtx.Done():
			// Authenticated or timed out
			return
		default:
			// Just continue
		}
		code = r.URL.Query().Get("code")

		if code != "" {
			cancel()

			err := srv.Shutdown(authCtx)
			if err != nil {
				log.Fatalf("Error shutting down auth server: %v\n", err)
			}

			a.updateCode(code)
			notify <- 1
		}
	})

	go func() {
		defer srv.Close()

		err := http.ListenAndServe(":3000", nil)
		if err != nil {
			log.Fatalf("Error running auth server: %v\n", err)
		}
	}()
}
