package oauth

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/Gleipnir-Technology/arcgis-go"
	"github.com/rs/zerolog/log"
	"source.gleipnir.technology/Gleipnir/nidus-sync/config"
	"source.gleipnir.technology/Gleipnir/nidus-sync/db"
	"source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/arcgis/model"
	"source.gleipnir.technology/Gleipnir/nidus-sync/db/models"
	queryarcgis "source.gleipnir.technology/Gleipnir/nidus-sync/db/query/arcgis"
	"source.gleipnir.technology/Gleipnir/nidus-sync/lint"
)

// When the API responds that the token is now invalidated
type InvalidatedTokenError struct{}

func (e InvalidatedTokenError) Error() string { return "The token has been invalidated by the server" }

type OAuthTokenResponse struct {
	AccessToken           string `json:"access_token"`
	ExpiresIn             int    `json:"expires_in"`
	RefreshToken          string `json:"refresh_token"`
	RefreshTokenExpiresIn int    `json:"refresh_token_expires_in"`
	SSL                   bool   `json:"ssl"`
	Username              string `json:"username"`
}

func DoTokenRequest(ctx context.Context, form url.Values) (*OAuthTokenResponse, error) {
	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
	defer cancel()

	form.Set("client_id", config.ClientID)

	baseURL := "https://www.arcgis.com/sharing/rest/oauth2/token/"
	req, err := http.NewRequestWithContext(ctx, "POST", baseURL, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, fmt.Errorf("Failed to create request: %w", err)
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	client := http.Client{}
	log.Info().Str("url", req.URL.String()).Msg("POST")
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("Failed to do request: %w", err)
	}
	defer lint.LogOnErr(resp.Body.Close, "close response body")
	bodyBytes, err := io.ReadAll(resp.Body)
	log.Info().Int("status", resp.StatusCode).Msg("Token request")
	if resp.StatusCode >= http.StatusBadRequest {
		if err != nil {
			return nil, fmt.Errorf("Got status code %d and failed to read response body: %w", resp.StatusCode, err)
		}
		bodyString := string(bodyBytes)
		var errorResp arcgis.ErrorResponse
		if err := json.Unmarshal(bodyBytes, &errorResp); err == nil {
			if errorResp.Error.Code == 498 && errorResp.Error.Description == "invalidated refresh_token" {
				return nil, InvalidatedTokenError{}
			}
			return nil, fmt.Errorf("API response JSON error: %d: %d %s", resp.StatusCode, errorResp.Error.Code, errorResp.Error.Description)
		}
		return nil, fmt.Errorf("API returned error status %d: %s", resp.StatusCode, bodyString)
	}
	//logResponseHeaders(resp)
	var tokenResponse OAuthTokenResponse
	err = json.Unmarshal(bodyBytes, &tokenResponse)
	if err != nil {
		return nil, fmt.Errorf("Failed to unmarshal JSON: %w", err)
	}
	// Just because we got a 200-level status code doesn't mean it worked. Experience has taught us that
	// we can get errors without anything indicated in the headers or the status code
	if tokenResponse == (OAuthTokenResponse{}) {
		var errorResponse arcgis.ErrorResponse
		err = json.Unmarshal(bodyBytes, &errorResponse)
		if err != nil {
			return nil, fmt.Errorf("Failed to unmarshal error JSON: %w", err)
		}
		if errorResponse.Error.Code > 0 {
			return nil, errorResponse.AsError(ctx)
		}
	}
	log.Info().Str("refresh token", tokenResponse.RefreshToken).Str("access token", tokenResponse.AccessToken).Int("access expires", tokenResponse.ExpiresIn).Int("refresh expires", tokenResponse.RefreshTokenExpiresIn).Msg("Oauth token acquired")
	return &tokenResponse, nil
}

func FutureUTCTimestamp(secondsFromNow int) time.Time {
	return time.Now().UTC().Add(time.Duration(secondsFromNow) * time.Second)
}

func GetOAuthForOrg(ctx context.Context, org *models.Organization) (*model.OAuthToken, error) {
	users, err := org.User().All(ctx, db.PGInstance.BobDB)
	if err != nil {
		return nil, fmt.Errorf("Failed to query all users for org: %w", err)
	}
	for _, user := range users {
		oauths, err := queryarcgis.OAuthTokensForUser(ctx, int64(user.ID))
		if err != nil {
			return nil, fmt.Errorf("Failed to query all oauth tokens for org: %w", err)
		}
		for _, oauth := range oauths {
			return &oauth, nil
		}
	}
	return nil, nil
}

// Update the access token to keep it fresh and alive
func RefreshAccessToken(ctx context.Context, oauth *model.OAuthToken) error {
	form := url.Values{
		"grant_type":    []string{"refresh_token"},
		"client_id":     []string{config.ClientID},
		"refresh_token": []string{oauth.RefreshToken},
	}
	token, err := DoTokenRequest(ctx, form)
	if err != nil {
		return fmt.Errorf("Failed to handle request: %w", err)
	}
	accessExpires := FutureUTCTimestamp(token.ExpiresIn)
	model := model.OAuthToken{
		AccessToken:        token.AccessToken,
		AccessTokenExpires: accessExpires,
		Username:           token.Username,
	}
	err = queryarcgis.OAuthTokenUpdateAccessToken(ctx, int64(oauth.ID), model)
	if err != nil {
		return fmt.Errorf("Failed to update oauth in database: %w", err)
	}
	log.Info().Int("oauth token id", int(oauth.ID)).Msg("Updated oauth token")
	return nil
}

// Update the refresh token to keep it fresh and alive
func RefreshRefreshToken(ctx context.Context, oauth *model.OAuthToken) error {

	form := url.Values{
		"grant_type":    []string{"exchange_refresh_token"},
		"redirect_uri":  []string{config.ArcGISOauthRedirectURL()},
		"refresh_token": []string{oauth.RefreshToken},
	}

	token, err := DoTokenRequest(ctx, form)
	if err != nil {
		return fmt.Errorf("Failed to handle request: %w", err)
	}
	refreshExpires := FutureUTCTimestamp(token.ExpiresIn)
	model := model.OAuthToken{
		RefreshToken:        token.RefreshToken,
		RefreshTokenExpires: refreshExpires,
		Username:            token.Username,
	}
	err = queryarcgis.OAuthTokenUpdateRefreshToken(ctx, int64(oauth.ID), model)
	if err != nil {
		return fmt.Errorf("Failed to update oauth in database: %w", err)
	}
	log.Info().Int("oauth token id", int(oauth.ID)).Msg("Updated oauth token")
	return nil
}
