package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"slices"
	"strings"
	"sync/atomic"
	"syscall"
	"time"

	"github.com/StevanFreeborn/chirpy/internal/auth"
	"github.com/StevanFreeborn/chirpy/internal/database"
	"github.com/google/uuid"
	"github.com/joho/godotenv"
	_ "github.com/lib/pq"
)

type server struct {
	fileServerHits atomic.Int32
	database       *database.Queries
	platform       string
	jwtSecret      []byte
	polkaKey       string
}

func writeJsonResponse(w http.ResponseWriter, statusCode int, response any) {
	if response == nil {
		w.WriteHeader(statusCode)
		return
	}

	data, err := json.Marshal(response)

	if err != nil {
		w.Header().Add("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)

		err := apiError{
			Err: "Failed to encode JSON response",
		}

		errData, _ := json.Marshal(err)

		w.Write(errData)
		return
	}

	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	w.Write(data)
}

func decodeJsonRequest[T any](r *http.Request) (*T, error) {
	defer r.Body.Close()
	var data *T
	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&data)

	if err != nil {
		return nil, apiError{
			Err: "Failed to deserialize request",
		}
	}

	return data, err
}

func (s *server) HandleFiles(prefix string) http.Handler {
	fileServer := http.StripPrefix(prefix, http.FileServer(http.Dir(".")))

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s.fileServerHits.Add(1)
		fileServer.ServeHTTP(w, r)
	})
}

func (s *server) HandleHealthChecks(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Content-Type", "text/plain; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}

func (s *server) HandleReset(w http.ResponseWriter, r *http.Request) {
	if s.platform != "dev" {
		w.WriteHeader(http.StatusForbidden)
		return
	}

	s.fileServerHits.Store(0)

	s.database.DeleteAllUsers(r.Context())

	w.Header().Add("Content-Type", "text/plain; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}

func (s *server) HandleMetrics(w http.ResponseWriter, r *http.Request) {
	hits := s.fileServerHits.Load()

	w.Header().Add("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusOK)

	template := `
	<html>
	  <body>
		  <h1>Welcome, Chirpy Admin</h1>
			<p>Chirpy has been visited %d times!</p>
		</body>
	</html>
	`

	fmt.Fprintf(w, template, hits)
}

type apiError struct {
	Err string `json:"error"`
}

func (e apiError) Error() string {
	return e.Err
}

type createChirpRequest struct {
	Body   string `json:"body"`
	UserId string `json:"user_id"`
}

type chirpResponse struct {
	Id        string `json:"id"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
	Body      string `json:"body"`
	UserId    string `json:"user_id"`
}

func (s *server) HandleCreateChirp(w http.ResponseWriter, r *http.Request) {
	blacklist := []string{
		"kerfuffle",
		"sharbert",
		"fornax",
	}

	bearerToken, err := auth.GetBearerToken(r.Header)

	unauthorizedError := apiError{
		Err: "You are not authorized to perform this action",
	}

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError)
		return
	}

	requestUserId, err := auth.ValidateJWT(bearerToken, s.jwtSecret)

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError)
		return
	}

	createChirpRequest, err := decodeJsonRequest[createChirpRequest](r)

	if err != nil {
		writeJsonResponse(w, http.StatusBadRequest, err)
		return
	}

	if len(createChirpRequest.Body) > 140 {
		writeJsonResponse(w, http.StatusBadRequest, apiError{
			Err: "Chirp is too long",
		})
		return
	}

	words := strings.Split(createChirpRequest.Body, " ")
	sanitized := []string{}

	for _, word := range words {
		if slices.Contains(blacklist, strings.ToLower(word)) {
			sanitized = append(sanitized, "****")
			continue
		}

		sanitized = append(sanitized, word)
	}

	cleanedBody := strings.Join(sanitized, " ")

	createChirpParams := database.CreateChirpParams{
		Body:   cleanedBody,
		UserID: requestUserId,
	}

	createdChirp, err := s.database.CreateChirp(r.Context(), createChirpParams)

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, apiError{
			Err: "Failed to create chirp. 🤷🏻‍♂️",
		})
		return
	}

	writeJsonResponse(w, http.StatusCreated, chirpResponse{
		Id:        createdChirp.ID.String(),
		CreatedAt: createdChirp.CreatedAt.Format(time.RFC3339),
		UpdatedAt: createdChirp.UpdatedAt.Format(time.RFC3339),
		Body:      createdChirp.Body,
		UserId:    createdChirp.UserID.String(),
	})
}

type createUserRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type createUserResponse struct {
	Id          string `json:"id"`
	CreatedAt   string `json:"created_at"`
	UpdatedAt   string `json:"updated_at"`
	Email       string `json:"email"`
	IsChirpyRed bool   `json:"is_chirpy_red"`
}

func (s *server) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
	createUserRequest, err := decodeJsonRequest[createUserRequest](r)

	if err != nil {
		writeJsonResponse(w, http.StatusBadRequest, err)
		return
	}

	trimmedEmail := strings.TrimSpace(createUserRequest.Email)

	if strings.TrimSpace(trimmedEmail) == "" {
		writeJsonResponse(w, http.StatusBadRequest, apiError{
			Err: "email is required. must be valid email address.",
		})
		return
	}

	trimmedPassword := strings.TrimSpace(createUserRequest.Password)

	if strings.TrimSpace(trimmedEmail) == "" {
		writeJsonResponse(w, http.StatusBadRequest, apiError{
			Err: "password is required. must be non-empty string.",
		})
		return
	}

	hashed_password, err := auth.HashPassword(trimmedPassword)

	createUserError := apiError{
		Err: "Uh oh we were unable to create a new user",
	}

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, createUserError)
		return
	}

	createUserParams := database.CreateUserParams{
		Email:          trimmedEmail,
		HashedPassword: hashed_password,
	}

	createdUser, err := s.database.CreateUser(r.Context(), createUserParams)

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, createUserError)
		return
	}

	writeJsonResponse(w, http.StatusCreated, createUserResponse{
		Id:          createdUser.ID.String(),
		CreatedAt:   createdUser.CreatedAt.Format(time.RFC3339),
		UpdatedAt:   createdUser.UpdatedAt.Format(time.RFC3339),
		Email:       createdUser.Email,
		IsChirpyRed: createdUser.IsChirpyRed,
	})
}

func (s *server) HandleGetAllChirps(w http.ResponseWriter, r *http.Request) {
	existingChirps, err := s.database.GetAllChirps(r.Context())

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, apiError{
			Err: "Unable to retrieve chirps",
		})
		return
	}

	chirps := []chirpResponse{}

	for _, c := range existingChirps {
		chirps = append(chirps, chirpResponse{
			Id:        c.ID.String(),
			CreatedAt: c.CreatedAt.Format(time.RFC3339),
			UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
			Body:      c.Body,
			UserId:    c.UserID.String(),
		})
	}

	writeJsonResponse(w, http.StatusOK, chirps)
}

func (s *server) HandleGetChirp(w http.ResponseWriter, r *http.Request) {
	chirpId := r.PathValue("id")
	validChirpId, err := uuid.Parse(chirpId)

	if err != nil {
		writeJsonResponse(w, http.StatusBadRequest, apiError{
			Err: "Chirp id is not valid. id must be valid UUID.",
		})
		return
	}

	existingChirp, err := s.database.GetChirpById(r.Context(), validChirpId)

	if err != nil {
		writeJsonResponse(w, http.StatusNotFound, apiError{
			Err: "No chirp with given id found.",
		})
		return
	}

	writeJsonResponse(w, http.StatusOK, chirpResponse{
		Id:        existingChirp.ID.String(),
		CreatedAt: existingChirp.CreatedAt.Format(time.RFC3339),
		UpdatedAt: existingChirp.UpdatedAt.Format(time.RFC3339),
		Body:      existingChirp.Body,
		UserId:    existingChirp.UserID.String(),
	})
}

type loginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

func (r *loginRequest) Validate() error {
	if strings.TrimSpace(r.Email) == "" || strings.TrimSpace(r.Password) == "" {
		return apiError{
			Err: "Email and password must be non-empty string",
		}
	}

	return nil
}

type loginResponse struct {
	Id           string `json:"id"`
	CreatedAt    string `json:"created_at"`
	UpdatedAt    string `json:"updated_at"`
	Email        string `json:"email"`
	IsChirpyRed  bool   `json:"is_chirpy_red"`
	Token        string `json:"token"`
	RefreshToken string `json:"refresh_token"`
}

func (s *server) HandleLogin(w http.ResponseWriter, r *http.Request) {
	loginRequest, err := decodeJsonRequest[loginRequest](r)

	err = loginRequest.Validate()

	if err != nil {
		writeJsonResponse(w, http.StatusBadRequest, err)
		return
	}

	existingUser, err := s.database.GetUserByEmail(r.Context(), loginRequest.Email)

	invalidLoginError := apiError{
		Err: "Invalid login request",
	}

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, invalidLoginError)
		return
	}

	isCorrectPassword, err := auth.CheckPasswordHash(loginRequest.Password, existingUser.HashedPassword)

	if err != nil || isCorrectPassword == false {
		writeJsonResponse(w, http.StatusUnauthorized, invalidLoginError)
		return
	}

	SECONDS_PER_HOUR := 3600
	expiresInDuration := time.Duration(SECONDS_PER_HOUR) * time.Second

	accessToken, err := auth.MakeJWT(existingUser.ID, s.jwtSecret, expiresInDuration)

	loginFailedError := apiError{
		Err: "Login failed",
	}

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, loginFailedError)
		return
	}

	createRefreshTokenParams := database.CreateRefreshTokenParams{
		Token:     auth.MakeRefreshToken(),
		ExpiresAt: time.Now().Add(60 * 24 * time.Hour),
		RevokedAt: sql.NullTime{
			Valid: false,
		},
		UserID: existingUser.ID,
	}

	createdRefreshToken, err := s.database.CreateRefreshToken(r.Context(), createRefreshTokenParams)

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, loginFailedError)
		return
	}

	writeJsonResponse(w, http.StatusOK, loginResponse{
		Id:           existingUser.ID.String(),
		CreatedAt:    existingUser.CreatedAt.Format(time.RFC3339),
		UpdatedAt:    existingUser.UpdatedAt.Format(time.RFC3339),
		Email:        existingUser.Email,
		IsChirpyRed:  existingUser.IsChirpyRed,
		Token:        accessToken,
		RefreshToken: createdRefreshToken.Token,
	})
}

type refreshTokenResponse struct {
	Token string `json:"token"`
}

func (s *server) HandleRefresh(w http.ResponseWriter, r *http.Request) {
	refreshToken, err := auth.GetBearerToken(r.Header)

	unauthorizedError := apiError{
		Err: "Unable to refresh token",
	}

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError)
		return
	}

	existingRefreshToken, err := s.database.GetRefreshTokenByToken(r.Context(), refreshToken)

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError)
		return
	}

	if existingRefreshToken.ExpiresAt.Before(time.Now()) || existingRefreshToken.RevokedAt.Valid {
		writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError)
		return
	}

	SECONDS_PER_HOUR := 3600
	expiresInDuration := time.Duration(SECONDS_PER_HOUR) * time.Second

	accessToken, err := auth.MakeJWT(existingRefreshToken.UserID, s.jwtSecret, expiresInDuration)

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, apiError{
			Err: "Failed to refresh token",
		})
		return
	}

	// TODO: We should rotate the refresh token

	writeJsonResponse(w, http.StatusOK, refreshTokenResponse{
		Token: accessToken,
	})
}

func (s *server) HandleRevoke(w http.ResponseWriter, r *http.Request) {
	refreshToken, err := auth.GetBearerToken(r.Header)

	if err != nil {
		writeJsonResponse(w, http.StatusBadRequest, apiError{
			Err: "No refresh token present in request",
		})
		return
	}

	existingRefreshToken, err := s.database.GetRefreshTokenByToken(r.Context(), refreshToken)

	if err != nil {
		writeJsonResponse(w, http.StatusNotFound, apiError{
			Err: "Unable to revoke token",
		})
		return
	}

	err = s.database.RevokeRefreshToken(r.Context(), existingRefreshToken.Token)

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, apiError{
			Err: "Unable to revoke token",
		})
		return
	}

	writeJsonResponse(w, http.StatusNoContent, nil)
}

type updateUserRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type updateUserResponse struct {
	Id          string `json:"id"`
	CreatedAt   string `json:"created_at"`
	UpdatedAt   string `json:"updated_at"`
	Email       string `json:"email"`
	IsChirpyRed bool   `json:"is_chirpy_red"`
}

func (r *updateUserRequest) Validate() error {
	if strings.TrimSpace(r.Email) == "" || strings.TrimSpace(r.Password) == "" {
		return apiError{
			Err: "Email and password must be non-empty string",
		}
	}

	return nil
}

func (s *server) HandleUpdateUser(w http.ResponseWriter, r *http.Request) {
	accessToken, err := auth.GetBearerToken(r.Header)

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, apiError{
			Err: "You are not authorized to perform this action",
		})
		return
	}

	requestUserId, err := auth.ValidateJWT(accessToken, s.jwtSecret)

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, apiError{
			Err: "You are not authorized to perform this action",
		})
		return
	}

	updateUserRequest, err := decodeJsonRequest[updateUserRequest](r)

	if err != nil {
		writeJsonResponse(w, http.StatusBadRequest, err)
		return
	}

	err = updateUserRequest.Validate()

	if err != nil {
		writeJsonResponse(w, http.StatusBadRequest, err)
		return
	}

	updatedHashedPassword, err := auth.HashPassword(updateUserRequest.Password)

	updateError := apiError{
		Err: "Unable to perform update",
	}

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, updateError)
		return
	}

	updateUserParams := database.UpdateUserParams{
		ID:             requestUserId,
		Email:          updateUserRequest.Email,
		HashedPassword: updatedHashedPassword,
	}

	updatedUser, err := s.database.UpdateUser(r.Context(), updateUserParams)

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, updateError)
		return
	}

	writeJsonResponse(w, http.StatusOK, updateUserResponse{
		Id:          updatedUser.ID.String(),
		CreatedAt:   updatedUser.CreatedAt.Format(time.RFC3339),
		UpdatedAt:   updatedUser.UpdatedAt.Format(time.RFC3339),
		Email:       updatedUser.Email,
		IsChirpyRed: updatedUser.IsChirpyRed,
	})
}

func (s *server) HandleDeleteChirp(w http.ResponseWriter, r *http.Request) {
	accessToken, err := auth.GetBearerToken(r.Header)

	unauthorizedError := apiError{
		Err: "You are not authorized to perform this action",
	}

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError)
		return
	}

	requestUserId, err := auth.ValidateJWT(accessToken, s.jwtSecret)

	if err != nil {
		writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError)
		return
	}

	chirpId := r.PathValue("id")
	validChirpId, err := uuid.Parse(chirpId)

	invalidChirpIdError := apiError{
		Err: "Chirp id must be a valid uuid for an existing chirp",
	}

	if err != nil {
		writeJsonResponse(w, http.StatusBadRequest, invalidChirpIdError)
		return
	}

	existingChirp, err := s.database.GetChirpById(r.Context(), validChirpId)

	if err != nil {
		writeJsonResponse(w, http.StatusNotFound, invalidChirpIdError)
		return
	}

	if existingChirp.UserID != requestUserId {
		writeJsonResponse(w, http.StatusForbidden, apiError{
			Err: "You can not delete a chirp that does not belong to you",
		})
		return
	}

	err = s.database.DeleteChirpById(r.Context(), existingChirp.ID)

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, apiError{
			Err: "Yo bro we couldn't delete that shit",
		})
		return
	}

	writeJsonResponse(w, http.StatusNoContent, nil)
}

type userUpgradeData struct {
	UserId string `json:"user_id"`
}

type webhookEvent struct {
	Event string `json:"event"`
	Data  any    `json:"-"`
}

func (p *webhookEvent) UnmarshalJSON(b []byte) error {
	type Alias webhookEvent

	aux := &struct {
		*Alias
		RawData json.RawMessage `json:"data"`
	}{
		Alias: (*Alias)(p),
	}

	if err := json.Unmarshal(b, aux); err != nil {
		return err
	}

	switch p.Event {
	case "user.upgraded":
		var data userUpgradeData

		if err := json.Unmarshal(aux.RawData, &data); err != nil {
			return err
		}

		p.Data = data
	default:
	}

	return nil
}

func (s *server) HandleWebhooks(w http.ResponseWriter, r *http.Request) {
	apiKey, err := auth.GetAPIKey(r.Header)

	unauthorizedError := apiError{
		Err: "You are not authorized to perform this action",
	}

	if err != nil || apiKey != s.polkaKey {
		writeJsonResponse(w, http.StatusUnauthorized, unauthorizedError)
		return
	}

	event, err := decodeJsonRequest[webhookEvent](r)

	if err != nil {
		writeJsonResponse(w, http.StatusInternalServerError, apiError{
			Err: "Unable to deserialize webhook event",
		})
		return
	}

	switch v := event.Data.(type) {
	case userUpgradeData:
		validUserId, err := uuid.Parse(v.UserId)

		userNotFoundError := apiError{
			Err: "Unable to upgrade user",
		}

		if err != nil {
			writeJsonResponse(w, http.StatusNotFound, userNotFoundError)
			return
		}

		_, err = s.database.UpgradeUser(r.Context(), validUserId)

		if err != nil {
			writeJsonResponse(w, http.StatusNotFound, userNotFoundError)
			return
		}

		writeJsonResponse(w, http.StatusNoContent, nil)
	default:
		writeJsonResponse(w, http.StatusNoContent, nil)
	}
}

func main() {
	err := godotenv.Load()

	if err != nil {
		log.Fatalf("Failed to load environment variables")
		os.Exit(1)
	}

	dbURL := os.Getenv("DB_URL")
	platform := os.Getenv("PLATFORM")
	jwtSecret := os.Getenv("JWT_SECRET")
	polkaKey := os.Getenv("POLKA_KEY")

	if strings.TrimSpace(jwtSecret) == "" {
		log.Fatalf("Failed to retrieve JWT_SECRET")
		os.Exit(1)
	}

	if strings.TrimSpace(polkaKey) == "" {
		log.Fatalf("Failed to retrieve POLKA_KEY")
		os.Exit(1)
	}

	db, err := sql.Open("postgres", dbURL)

	if err != nil {
		log.Fatalf("Failed to connect to database: %v", err)
		os.Exit(1)
	}

	server := &server{
		database:  database.New(db),
		platform:  platform,
		jwtSecret: []byte(jwtSecret),
		polkaKey:  polkaKey,
	}

	mux := http.NewServeMux()

	appRoot := "/app/"

	mux.Handle(appRoot, server.HandleFiles(appRoot))
	mux.HandleFunc("GET /admin/metrics", server.HandleMetrics)
	mux.HandleFunc("POST /admin/reset", server.HandleReset)

	mux.HandleFunc("GET /api/healthz", server.HandleHealthChecks)

	mux.HandleFunc("POST /api/login", server.HandleLogin)
	mux.HandleFunc("POST /api/refresh", server.HandleRefresh)
	mux.HandleFunc("POST /api/revoke", server.HandleRevoke)
	mux.HandleFunc("POST /api/users", server.HandleCreateUser)
	mux.HandleFunc("PUT /api/users", server.HandleUpdateUser)
	mux.HandleFunc("POST /api/polka/webhooks", server.HandleWebhooks)

	mux.HandleFunc("GET /api/chirps", server.HandleGetAllChirps)
	mux.HandleFunc("GET /api/chirps/{id}", server.HandleGetChirp)
	mux.HandleFunc("POST /api/chirps", server.HandleCreateChirp)
	mux.HandleFunc("DELETE /api/chirps/{id}", server.HandleDeleteChirp)

	port := ":8080"

	httpServer := http.Server{
		Handler: mux,
		Addr:    port,
	}

	go func() {
		log.Printf("Server started and listening on %s\n", port)

		err := httpServer.ListenAndServe()

		if !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("HTTP Server error: %v", err)
		}

		log.Println("Stopped serving new connections")
	}()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	<-sigChan

	shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)

	defer shutdownRelease()

	if err := httpServer.Shutdown(shutdownCtx); err != nil {
		log.Fatalf("HTTP shutdown error: %v", err)
	}

	log.Println("Graceful shutdown complete")
}
