package pogs

import (
	"errors"
	"time"
)

// AuthenticateResponse is the serialized response from the Authenticate RPC.
// It's a 1:1 representation of the capnp message, so it's not very useful for programmers.
// Instead, you should call the `Outcome()` method to get a programmer-friendly sum type, with one
// case for each possible outcome.
type AuthenticateResponse struct {
	PermanentErr      string
	RetryableErr      string
	Jwt               []byte
	HoursUntilRefresh uint8
}

// Outcome turns the deserialized response of Authenticate into a programmer-friendly sum type.
func (ar AuthenticateResponse) Outcome() AuthOutcome {
	// If the user's authentication was unsuccessful, the server will return an error explaining why.
	// cloudflared should fatal with this error.
	if ar.PermanentErr != "" {
		return NewAuthFail(errors.New(ar.PermanentErr))
	}

	// If there was a network error, then cloudflared should retry later,
	// because origintunneld couldn't prove whether auth was correct or not.
	if ar.RetryableErr != "" {
		return NewAuthUnknown(errors.New(ar.RetryableErr), ar.HoursUntilRefresh)
	}

	// If auth succeeded, return the token and refresh it when instructed.
	if len(ar.Jwt) > 0 {
		return NewAuthSuccess(ar.Jwt, ar.HoursUntilRefresh)
	}

	// Otherwise the state got messed up.
	return nil
}

// AuthOutcome is a programmer-friendly sum type denoting the possible outcomes of Authenticate.
type AuthOutcome interface {
	isAuthOutcome()
	// Serialize into an AuthenticateResponse which can be sent via Capnp
	Serialize() AuthenticateResponse
}

// AuthSuccess means the backend successfully authenticated this cloudflared.
type AuthSuccess struct {
	jwt               []byte
	hoursUntilRefresh uint8
}

func NewAuthSuccess(jwt []byte, hoursUntilRefresh uint8) AuthSuccess {
	return AuthSuccess{jwt: jwt, hoursUntilRefresh: hoursUntilRefresh}
}

func (ao AuthSuccess) JWT() []byte {
	return ao.jwt
}

// RefreshAfter is how long cloudflared should wait before rerunning Authenticate.
func (ao AuthSuccess) RefreshAfter() time.Duration {
	return hoursToTime(ao.hoursUntilRefresh)
}

// Serialize into an AuthenticateResponse which can be sent via Capnp
func (ao AuthSuccess) Serialize() AuthenticateResponse {
	return AuthenticateResponse{
		Jwt:               ao.jwt,
		HoursUntilRefresh: ao.hoursUntilRefresh,
	}
}

func (ao AuthSuccess) isAuthOutcome() {}

// AuthFail means this cloudflared has the wrong auth and should exit.
type AuthFail struct {
	err error
}

func NewAuthFail(err error) AuthFail {
	return AuthFail{err: err}
}

func (ao AuthFail) Error() string {
	return ao.err.Error()
}

// Serialize into an AuthenticateResponse which can be sent via Capnp
func (ao AuthFail) Serialize() AuthenticateResponse {
	return AuthenticateResponse{
		PermanentErr: ao.err.Error(),
	}
}

func (ao AuthFail) isAuthOutcome() {}

// AuthUnknown means the backend couldn't finish checking authentication. Try again later.
type AuthUnknown struct {
	err               error
	hoursUntilRefresh uint8
}

func NewAuthUnknown(err error, hoursUntilRefresh uint8) AuthUnknown {
	return AuthUnknown{err: err, hoursUntilRefresh: hoursUntilRefresh}
}

func (ao AuthUnknown) Error() string {
	return ao.err.Error()
}

// RefreshAfter is how long cloudflared should wait before rerunning Authenticate.
func (ao AuthUnknown) RefreshAfter() time.Duration {
	return hoursToTime(ao.hoursUntilRefresh)
}

// Serialize into an AuthenticateResponse which can be sent via Capnp
func (ao AuthUnknown) Serialize() AuthenticateResponse {
	return AuthenticateResponse{
		RetryableErr:      ao.err.Error(),
		HoursUntilRefresh: ao.hoursUntilRefresh,
	}
}

func (ao AuthUnknown) isAuthOutcome() {}

func hoursToTime(hours uint8) time.Duration {
	return time.Duration(hours) * time.Hour
}