package main

import (
 "fmt"
 "time"
 "net/http"
 "io"
 "os"
 "context"
 "log"
 "io/ioutil"
 "strings"
 "encoding/json"
 "math/rand"
 openai "github.com/sashabaranov/go-openai"
 "github.com/stripe/stripe-go/v74"
 "github.com/stripe/stripe-go/v74/event"
 "github.com/stripe/stripe-go/v74/webhook"
 "github.com/stripe/stripe-go/v74/checkout/session"

 "gorm.io/gorm"
 "gorm.io/driver/sqlite"

 "github.com/creikey/rpgpt/server/codes"
)

// BoughtType values. do not reorganize these or you fuck up the database
const (
 DayPass = iota
)

const (
 // A-Z and 0-9, four digits means this many codes
 MaxCodes = 36 * 36 * 36 * 36
 MaxTime = 10*60  *1000 // in seconds
)


type User struct {
 CreatedAt time.Time
 UpdatedAt time.Time
 DeletedAt gorm.DeletedAt `gorm:"index"`

 Code  codes.UserCode `gorm:"primaryKey"` // of maximum value max codes, incremented one by one. These are converted to 4 digit alphanumeric code users can remember/use
 BoughtTime int64 // unix time. Used to figure out if the pass is still valid
 BoughtType int // enum

 IsFulfilled bool // before users are checked out they are unfulfilled
 CheckoutSessionID string
}

type ChatGPTElem struct {
 ElemType string `json:"type"`
 Content string `json:"Content"`
}

var c *openai.Client
var logResponses = false
var doCors = false
var checkoutRedirectTo string
var daypassPriceId string
var webhookSecret string
var db *gorm.DB
var daypassTimedOut = make(map[codes.UserCode]int64) // value is time last requested, rate limiting by day pass. If exists is rate limited, should be removed when ok to request again
// for 10 free minutes a day, is when ip address began requesting
var ipAddyTenFree = make(map[string]int64)

func cleanTimedOut() {
 for k, v := range daypassTimedOut {
  if currentTime() - v > 1 {
   delete(daypassTimedOut, k)
  }
 }
 for k, v := range ipAddyTenFree {
  if currentTime() - v > 24*60*60 {
   delete(ipAddyTenFree, k)
  }
 }
}


func isUserOld(user User) bool {
 return (currentTime() - user.BoughtTime) > 24*60*60
}

func clearOld(db *gorm.DB) {
 var users []User
 result := db.Find(&users)
 if result.Error != nil {
  log.Fatal(result.Error)
 }
 var toDelete []codes.UserCode // codes
 for _, user := range users {
  if user.BoughtType != 0 {
   panic("Don't know how to handle bought type " + string(user.BoughtType) + " yet")
  }
  if isUserOld(user) {
   toDelete = append(toDelete, user.Code)
  }
 }

 for _, del := range toDelete {
  db.Delete(&User{}, del)
 }
}

func handleEvent(event stripe.Event) error {
 if len(stripe.Key) == 0 {
  log.Printf("Received stripe event %s , but stripe capabilities are disabled", event)
  return nil
 }

 if event.Type == "checkout.session.completed" {
  var session stripe.CheckoutSession
  err := json.Unmarshal(event.Data.Raw, &session)
  if err != nil {
   return fmt.Errorf("Error parsing webhook JSON %s", err)
  }

  params := &stripe.CheckoutSessionParams{}
  params.AddExpand("line_items")

  // Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
  // Fulfill the purchase...

  var toFulfill User
  found := false
  for trial := 0; trial < 5; trial++ {
   if db.Where("checkout_session_id = ?", session.ID).First(&toFulfill).Error != nil {
    log.Println("Failed to fulfill user with ID " + session.ID)
   } else {
    found = true
    break
   }
  }
  if !found {
   return fmt.Errorf("Error Failed to find user in database to fulfill: very bad! ID: " + session.ID)
  } else {
   userString, err := codes.CodeToString(toFulfill.Code)
   if err != nil {
    return fmt.Errorf("Error strange thing, saved user's code was unable to be converted to a string %s", err)
   }
   log.Printf("Fulfilling user with code %s number %d\n", userString, toFulfill.Code)
   if(toFulfill.IsFulfilled) {
    log.Printf("User with code %s is already fulfilled, strange\n", userString)
   }
   toFulfill.IsFulfilled = true
   err = db.Save(&toFulfill).Error
   if err != nil {
    return fmt.Errorf("Failed to save fulfilled flag status to database: %s", err)
   }
  }
 }
 return nil
}

func webhookResponse(w http.ResponseWriter, req *http.Request) {
 if len(stripe.Key) == 0 {
  log.Printf("Received webhook response %s , but stripe capabilities are disableda", req)
  return
 }

 const MaxBodyBytes = int64(65536)
 req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)

 body, err := ioutil.ReadAll(req.Body)
 if err != nil {
  log.Printf("Error reading request body: %v\n", err)
  w.WriteHeader(http.StatusServiceUnavailable)
  return
 }

 endpointSecret := webhookSecret
 event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), endpointSecret)

 if err != nil {
  log.Printf("Error verifying webhook signature %s\n", err)
  w.WriteHeader(http.StatusBadRequest) // Return a 400 error on a bad signature
  return
 }

 err = handleEvent(event)
 if err != nil {
  w.WriteHeader(http.StatusBadRequest)
 }
 
 w.WriteHeader(http.StatusOK)
}

func checkout(w http.ResponseWriter, req *http.Request) {
 if doCors {
  w.Header().Set("Access-Control-Allow-Origin", "*")
 }

 // generate a code
 var newCode string
 var newCodeUser codes.UserCode
 found := false
 for i := 0; i < 1000; i++ {
  codeInt := rand.Intn(MaxCodes)
  newCodeUser = codes.UserCode(codeInt)
  var tmp User
  r := db.Where("Code = ?", codeInt).Limit(1).Find(&tmp)
  if r.RowsAffected == 0{
   var err error
   newCode, err = codes.CodeToString(newCodeUser)
   if err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    log.Fatalf("Failed to generate code from random number: %s", err)
    return
   }
   found = true
   break
  }
 }
 if !found {
  w.WriteHeader(http.StatusInternalServerError)
  log.Fatal("Failed to find new code!!!")
  return
 }

 customMessage := fmt.Sprintf("**IMPORTANT** Your Day Pass Code is %s", newCode)
 redirecting := "https://google.com"
 if len(checkoutRedirectTo) > 0 {
	 redirecting = checkoutRedirectTo
 }
 params := &stripe.CheckoutSessionParams {
  LineItems: []*stripe.CheckoutSessionLineItemParams {
   &stripe.CheckoutSessionLineItemParams{
    Price: stripe.String(daypassPriceId),
    Quantity: stripe.Int64(1),
   },
  },
  Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
  SuccessURL: stripe.String(redirecting),
  CancelURL: stripe.String(redirecting),
  CustomText: &stripe.CheckoutSessionCustomTextParams{ Submit: &stripe.CheckoutSessionCustomTextSubmitParams { Message: &customMessage } },
 }

 s, err := session.New(params)

 if err != nil {
  log.Printf("session.New: %v", err)
 }

 log.Printf("Creating user code %s code integer %d with checkout session ID %s\n", newCode, newCodeUser ,s.ID)
 result := db.Create(&User {
  Code: newCodeUser,
  BoughtTime: currentTime(),
  BoughtType: DayPass,
  IsFulfilled: false,
  CheckoutSessionID: s.ID,
 })
 if result.Error != nil {
  w.WriteHeader(http.StatusInternalServerError)
  log.Printf("Failed to write to database: %s", result.Error)
 } else {
  log.Printf("SUccessfully created usercode!\n")
  fmt.Fprintf(w, "%s|%s", newCode, s.URL)
 }
}

func completion(w http.ResponseWriter, req *http.Request) {
 logStr := "" // print all the logs at once, to make it so the printed output as multiple completions are happening at once, is coherent
 defer func() {
  if len(logStr) > 0 {
   log.Println(logStr)
  }
 }()

 req.Body = http.MaxBytesReader(w, req.Body, 1024 * 1024) // no sending huge files to crash the server
 if doCors {
  w.Header().Set("Access-Control-Allow-Origin", "*")
 }
 bodyBytes, err := io.ReadAll(req.Body)
 if err != nil {
  w.WriteHeader(http.StatusBadRequest)
  logStr += fmt.Sprintf("Bad error: ", err) + "\n"
  return
 } else {
  bodyString := string(bodyBytes)
  splitBody := strings.Split(bodyString, "|")

  if len(splitBody) != 2 {
   w.WriteHeader(http.StatusBadRequest)
   logStr += fmt.Sprintf("Weird body length %d not 2\n", len(splitBody))
   return
  }
  var promptString string = splitBody[1]
  var userToken string = splitBody[0]

  // see if need to pay
  rejected := false
  cleanTimedOut()
  if len(stripe.Key) == 0 {
   logStr += fmt.Sprintf("Stripe capabilities are disabled, so automatically allowing completion")
  } else {
   if len(userToken) != 4 {
    // where I do the IP rate limiting

    userKey := req.RemoteAddr
    if req.Header.Get("X-Forwarded-For") != "" {
     userKey = req.Header.Get("X-Forwarded-For")
    }
    createdTime, ok := ipAddyTenFree[userKey]
    if !ok {
     ipAddyTenFree[userKey] = currentTime()
     rejected = false
    } else {
     if currentTime() - createdTime < MaxTime {
      rejected = false
     } else {
      rejected = true // out of free time buddy
     }
    }
   } else {
    var thisUser User
    thisUserCode, err := codes.ParseUserCode(userToken)
    if err != nil {
     logStr += fmt.Sprintf("Error: Failed to parse user token %s\n", userToken)
     rejected = true
    } else {
     err := db.First(&thisUser, thisUserCode).Error 
     if err != nil {
      logStr += fmt.Sprintf("User code %d string %s couldn't be found in the database: %s\n", thisUserCode, userToken, err)
      rejected = true
     } else {
      if isUserOld(thisUser) {
       logStr += fmt.Sprintf("User code " + userToken + " is old, not valid") + "\n"
       db.Delete(&thisUser)
       rejected = true
      } else {
       // now have valid user, in the database, to be rate limit checked
       // rate limiting based on user token
       if !thisUser.IsFulfilled {
        logStr += fmt.Sprintf("Unfulfilled user trying to play, might've been unresponded to event. Retrieving backlog of unfulfilled events...\n") + "\n"
        params := &stripe.EventListParams{}
        params.Filters.AddFilter("delivery_success", "", "false")
        i := event.List(params)
        for i.Next() {
         e := i.Event()
         logStr += fmt.Sprintf("Unfulfilled event! Of type %s. Handling...\n", e.Type) + "\n"
         err := handleEvent(*e)
         if err != nil {
          logStr += fmt.Sprintf("Failed to fulfill unfulfilled event: %s\n", err) + "\n"
         }
        }
       }
       if thisUser.IsFulfilled {
        _, exists := daypassTimedOut[thisUserCode]
        if exists {
         rejected = true
        } else {
         rejected = false
         daypassTimedOut[thisUserCode] = currentTime()
        }
       } else {
        logStr += fmt.Sprintf("User with code and existing entry in database was not fulfilled, and wanted to play... Very bad. Usercode: %s\n", thisUserCode) + "\n"
       }
      }
     }
    }
   }
  }
  if rejected {
   fmt.Fprintf(w, "0")
   return
  }

  if logResponses {
   logStr += fmt.Sprintf("Println line prompt string: ", promptString) + "\n"
  }

  ctx := context.Background()
  var response string = ""
  if false {
   req := openai.CompletionRequest {
    Model:     "davinci:ft-alnar-games-2023-04-03-10-06-45",
    MaxTokens: 80,
    Prompt:    promptString,
    Temperature: 0.9,
    FrequencyPenalty: 0.0,
    PresencePenalty: 0.6,
    TopP: 1.0,
    Stop: []string{"\n"},
    N: 1,
   }
   resp, err := c.CreateCompletion(ctx, req)
   if err != nil {
    logStr += fmt.Sprintf("Error Failed to generate, failed to create completion: ", err) + "\n"
    w.WriteHeader(http.StatusInternalServerError)
    return
   }
   response = resp.Choices[0].Text
  } else {
   // parse the json walter
   var parsed []ChatGPTElem
   logStr += fmt.Sprintf("----------------------------------------------------------")
   logStr += fmt.Sprintf("Parsing prompt string `%s`\n\n\n", promptString)
   err = json.Unmarshal([]byte(promptString), &parsed)
   if err != nil {
    logStr += fmt.Sprintf("Error bad json given for prompt: ", err) + "\n"
    w.WriteHeader(http.StatusBadRequest)
    return
   }

   messages := make([]openai.ChatCompletionMessage, 0)
   for _, elem := range parsed {
    logStr += fmt.Sprintf("Making message with role %s and Content `%s`...\n", elem.ElemType, elem.Content)
    messages = append(messages, openai.ChatCompletionMessage {
     Role: elem.ElemType,
     Content: elem.Content,
    })
   }
   logStr += fmt.Sprintf("----------------------------------------------------------")

	 if false { // temporary testing AI
		 response = "ACT_holo \"Garbage\""
	 } else {

		 resp, err := c.CreateChatCompletion(
			 context.Background(),
			 openai.ChatCompletionRequest{
				 Model: openai.GPT3Dot5Turbo,
				 Messages: messages,
				 Stop: []string{"\n"},
			 },
		 )
		 if err != nil {
			 logStr += fmt.Sprintf("Error Failed to generate: ", err) + "\n"
			 w.WriteHeader(http.StatusInternalServerError)
			 return
		 }

		 logStr += fmt.Sprintf("Full response: \n````\n%s\n````\n", resp)
		 response = resp.Choices[0].Message.Content
	 }

   /*
   with_action := strings.SplitAfter(response, "ACT_")
   if len(with_action) != 2 {
    logStr += fmt.Sprintf("Could not find action in response string `%s`\n", response)
    w.WriteHeader(http.StatusInternalServerError) // game should send a new retry request after this
    return
   }
   response = with_action[1]

   // trim ending quotation mark. There might be text after the ending quotation mark because chatgpt sometimes
   // puts addendums in its responses, like `ACT_none "Hey" (I wanted the NPC to say hey here)`. The stuffafter the second
   // quotation mark needs to be ignored
   between_quotes := strings.Split(response, "\"")
   // [action] " [stuff] " [anything extra]
   if len(between_quotes) < 2 {
     logStr += fmt.Sprintf("Could not find enough quotes in response string `%s`\n", response)
	 w.WriteHeader(http.StatusInternalServerError)
	 return
   }
   response = between_quotes[0] + "\"" + between_quotes[1] + "\""
   */
  }
  if logResponses {
   logStr += fmt.Sprintf("Println response: `", response + "`") + "\n"
   logStr += "\n"
  }
  fmt.Fprintf(w, "1%s", response + "\n")
 }
}

func currentTime() int64 {
 return time.Now().Unix()
}

func main() {
 rand.Seed(time.Now().UnixNano())
 var err error
 db, err = gorm.Open(sqlite.Open("rpgpt.db"), &gorm.Config{})
 if err != nil {
  log.Fatal(err)
 }
 
 db.AutoMigrate(&User{})

 clearOld(db)

 api_key := os.Getenv("OPENAI_API_KEY")
 if api_key == "" {
  log.Fatal("Must provide openai key")
 }
 checkoutRedirectTo = os.Getenv("REDIRECT_TO")
 if checkoutRedirectTo == "" {
	 log.Printf("No REDIRECT_TO environment variable, will just redirect to https://google.com")
 }
 stripeKey := os.Getenv("STRIPE_KEY")
 if stripeKey == "" {
  log.Printf("No stripe key provided, disabling payment behavior")
 }
 daypassPriceId = os.Getenv("PRICE_ID")
 if len(stripeKey) > 0 && daypassPriceId == "" {
  log.Fatal("Must provide daypass price ID")
 }
 if len(stripeKey) > 0 {
	 stripe.Key = stripeKey
 }
 webhookSecret = os.Getenv("WEBHOOK_SECRET")
 if len(stripeKey) > 0 && webhookSecret == "" {
  log.Fatal("Must provide webhook secret for receiving checkout completed events")
 }

 logResponses = os.Getenv("LOG_RESPONSES") != ""
 doCors = os.Getenv("CORS") != ""
 c = openai.NewClient(api_key)

 http.HandleFunc("/completion", completion)
 http.HandleFunc("/webhook", webhookResponse)
 http.HandleFunc("/checkout", checkout)

 portString := ":8090"
 log.Println("DO NOT RUN WITH CLOUDFLARE PROXY it rate limits based on IP, if behind proxy every IP will be the same. Would need to fix req.RemoteAddr. Serving on " + portString + "...")
 log.Fatal(http.ListenAndServe(portString, nil))
}

