package web

import (
	"bufio"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"go.buzzert.net/kordophone-mock/model"
	"go.buzzert.net/kordophone-mock/server"
	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
	"github.com/gorilla/websocket"
)

type MockHTTPServerConfiguration struct {
	AuthEnabled bool
}

type MockHTTPServer struct {
	Server      server.Server
	mux         http.ServeMux
	authEnabled bool
}

type AuthError struct {
	message string
}

func (e *AuthError) Error() string {
	return e.message
}

func (m *MockHTTPServer) logRequest(r *http.Request, extras ...string) {
	log.Debug().Msgf("%s %s   %s", r.Method, r.URL.Path, strings.Join(extras, " "))
}

func (m *MockHTTPServer) checkAuthentication(r *http.Request) error {
	if !m.authEnabled {
		return nil
	}

	// Check for Authorization header
	authHeader := r.Header.Get("Authorization")
	if authHeader == "" {
		return &AuthError{"Missing Authorization header"}
	}

	// Check for "Bearer" prefix
	if authHeader[:7] != "Bearer " {
		return &AuthError{"Invalid Authorization header"}
	}

	// Check for valid token
	token := authHeader[7:]
	if !m.Server.CheckBearerToken(token) {
		return &AuthError{"Invalid token"}
	}

	return nil
}

func (m *MockHTTPServer) requireAuthentication(w http.ResponseWriter, r *http.Request) bool {
	if !m.authEnabled {
		return true
	}

	if err := m.checkAuthentication(r); err != nil {
		log.Error().Err(err).Msg("Error checking authentication")
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return false
	}

	return true
}

func (m *MockHTTPServer) handleVersion(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "%s", m.Server.Version())
}

func (m *MockHTTPServer) handleStatus(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	fmt.Fprintf(w, "OK")
}

func (m *MockHTTPServer) handleConversations(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	convos := m.Server.Conversations()

	// Encode convos as JSON
	jsonData, err := json.Marshal(convos)
	if err != nil {
		log.Error().Err(err).Msg("Error marshalling conversations")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Write JSON to response
	w.Header().Set("Content-Type", "application/json")
	w.Write(jsonData)
}

func (m *MockHTTPServer) handleMessages(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	guid := r.URL.Query().Get("guid")
	if len(guid) == 0 {
		log.Error().Msg("handleMessage: Got empty guid parameter")
		http.Error(w, "no guid parameter specified", http.StatusBadRequest)
		return
	}

	conversation, err := m.Server.ConversationForGUID(guid)
	if err != nil {
		log.Error().Err(err).Msgf("handleMessage: Error getting conversation (%s)", guid)
		http.Error(w, "conversation not found", http.StatusBadRequest)
		return
	}

	beforeDate := r.URL.Query().Get("beforeDate")
	beforeGUID := r.URL.Query().Get("beforeMessageGUID")
	afterGUID := r.URL.Query().Get("afterMessageGUID")
	limit := r.URL.Query().Get("limit")

	stringOrNil := func(s string) *string {
		if len(s) == 0 {
			return nil
		}
		return &s
	}

	dateOrNil := func(s string) *model.Date {
		if len(s) == 0 {
			return nil
		}
		t, _ := time.Parse(time.RFC3339, s)
		date := model.Date(t)
		return &date
	}

	intOrNil := func(s string) *int {
		if len(s) == 0 {
			return nil
		}
		i, _ := strconv.Atoi(s)
		return &i
	}

	query := server.MessagesQuery{
		Limit:            intOrNil(limit),
		BeforeDate:       dateOrNil(beforeDate),
		BeforeGUID:       stringOrNil(beforeGUID),
		AfterGUID:        stringOrNil(afterGUID),
		ConversationGUID: conversation.Guid,
	}

	messages := m.Server.PerformMessageQuery(&query)

	jsonData, err := json.Marshal(messages)
	if err != nil {
		log.Error().Err(err).Msg("Error marshalling messages")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Write JSON to response
	w.Header().Set("Content-Type", "application/json")
	w.Write(jsonData)
}

func (m *MockHTTPServer) handleAuthenticate(w http.ResponseWriter, r *http.Request) {
	// Decode request body as AuthenticationRequest
	var authReq AuthenticationRequest
	err := json.NewDecoder(r.Body).Decode(&authReq)
	if err != nil {
		log.Error().Err(err).Msg("Authenticate: Error decoding request body")
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Authenticate
	token, err := m.Server.Authenticate(authReq.Username, authReq.Password)
	if err != nil {
		log.Error().Err(err).Msg("Authenticate: Error authenticating")
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}

	// Write response
	w.Header().Set("Content-Type", "application/json")

	// Encode token as JSON
	jsonData, err := json.Marshal(token)
	if err != nil {
		log.Error().Err(err).Msg("Error marshalling token")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Write JSON to response
	w.Write(jsonData)
}

func (m *MockHTTPServer) handleNotFound(w http.ResponseWriter, r *http.Request) {
	log.Error().Msgf("Unimplemented API endpoint: %s %s", r.Method, r.URL.Path)
	http.NotFound(w, r)
}

func (m *MockHTTPServer) handleSendMessage(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	// Decode request body as SendMessageRequest
	var sendMessageReq SendMessageRequest
	err := json.NewDecoder(r.Body).Decode(&sendMessageReq)
	if err != nil {
		log.Error().Err(err).Msg("SendMessage: Error decoding request body")
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Find conversation
	conversation, err := m.Server.ConversationForGUID(sendMessageReq.ConversationGUID)
	if err != nil {
		log.Error().Err(err).Msgf("SendMessage: Error finding conversation (%s)", sendMessageReq.ConversationGUID)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Create Message
	message := model.Message{
		Guid:            uuid.New().String(),
		Text:            sendMessageReq.Body,
		Date:            model.Date(time.Now()),
		AttachmentGUIDs: sendMessageReq.TransferGUIDs,
		Sender:          nil, // me
	}

	// Send message
	m.Server.SendMessage(conversation, message)

	// Encode response as JSON
	jsonData, err := json.Marshal(message)
	if err != nil {
		log.Error().Err(err).Msg("Error marshalling response")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Write JSON to response
	w.Header().Set("Content-Type", "application/json")
	w.Write(jsonData)
}

func (m *MockHTTPServer) handlePollUpdates(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	// TODO: This should block if we don't have updates for that seq yet.

	seq := -1
	seqString := r.URL.Query().Get("seq")
	if len(seqString) > 0 {
		var err error
		seq, err = strconv.Atoi(seqString)
		if err != nil {
			log.Error().Err(err).Msg("FetchUpdates: Error parsing seq")
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
	}

	// Fetch updates (blocking)
	updates := m.Server.FetchUpdatesBlocking(seq)

	if len(updates) == 0 {
		// return 205 (Nothing to report)
		w.WriteHeader(http.StatusResetContent)
		log.Info().Msg("FetchUpdates: Nothing to report")
	} else {
		// Encode updates as JSON
		jsonData, err := json.Marshal(updates)
		if err != nil {
			log.Error().Err(err).Msg("Error marshalling updates")
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Write JSON to response
		w.Header().Set("Content-Type", "application/json")
		w.Write(jsonData)
	}
}

func (m *MockHTTPServer) handleMarkConversation(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	guid := r.URL.Query().Get("guid")
	if len(guid) == 0 {
		log.Error().Msg("handleMarkConversation: Got empty guid parameter")
		http.Error(w, "no guid parameter specified", http.StatusBadRequest)
		return
	}

	// Find conversation
	convo, err := m.Server.ConversationForGUID(guid)
	if err != nil {
		log.Error().Err(err).Msgf("handleMarkConversation: Error finding conversation (%s)", guid)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Mark conversation
	m.Server.MarkConversationAsRead(convo)

	// Respond 200
	w.WriteHeader(http.StatusOK)
}

func (m *MockHTTPServer) handleFetchAttachment(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	guid := r.URL.Query().Get("guid")
	if guid == "" {
		log.Error().Msg("fetchAttachment: Missing 'guid' parameter")
		http.Error(w, "no guid parameter specified", http.StatusBadRequest)
		return
	}

	reader, err := m.Server.FetchAttachment(guid)
	if err != nil {
		log.Error().Msgf("fetchAttachment: Could not load attachment from store: %s", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	dw := bufio.NewWriter(w)
	_, err = dw.ReadFrom(reader)
	if err != nil {
		log.Error().Msgf("fetchAttachment: Error reading attachment data: %s", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func (m *MockHTTPServer) handleUploadAttachment(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	filename := r.URL.Query().Get("filename")
	if filename == "" {
		log.Error().Msgf("uploadAttachment: filename not provided")
		http.Error(w, "filename not provided", http.StatusBadRequest)
		return
	}

	guid, err := m.Server.UploadAttachment(filename, r.Body)
	if err != nil {
		log.Error().Msgf("uploadAttachment: error storing attachment: %s", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	response := UploadAttachmentResponse{
		TransferGUID: *guid,
	}

	jsonData, err := json.Marshal(response)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(jsonData)
}

func (m *MockHTTPServer) handleUpdates(w http.ResponseWriter, r *http.Request) {
	if !m.requireAuthentication(w, r) {
		return
	}

	upgrader := websocket.Upgrader{
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	}

	c, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Error().Err(err).Msg("websocket upgrade failed")
		return
	}
	m.handleUpdatesWebsocket(c)
}

func (m *MockHTTPServer) handleUpdatesWebsocket(c *websocket.Conn) {
	// Fetch updates continuously
	defer c.Close()

	
	// Start a goroutine to handle incoming messages (pings, usually)
	go func() {
		for {
			_, _, err := c.ReadMessage()
			if err != nil {
				log.Error().Err(err).Msg("WebSocket read error")
				return
			}
		}
	}()	
	
	for {
		// Fetch updates (blocking)
		updates := m.Server.FetchUpdatesBlocking(-1)

		// Send updates to client
		err := c.WriteJSON(updates)
		if err != nil {
			log.Error().Err(err).Msg("handleUpdatesWebsocket: Error sending updates to client (probably disconnected)")
			return
		}
	}
}

func (m *MockHTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	m.logRequest(r, r.URL.Query().Encode())
	m.mux.ServeHTTP(w, r)
}

func NewMockHTTPServer(config MockHTTPServerConfiguration) *MockHTTPServer {
	this := MockHTTPServer{
		Server:      *server.NewServer(),
		mux:         *http.NewServeMux(),
		authEnabled: config.AuthEnabled,
	}

	// Redirect /api/* to /*
	this.mux.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, r.URL.Path, http.StatusMovedPermanently)
	})))

	this.mux.Handle("/version", http.HandlerFunc(this.handleVersion))
	this.mux.Handle("/conversations", http.HandlerFunc(this.handleConversations))
	this.mux.Handle("/status", http.HandlerFunc(this.handleStatus))
	this.mux.Handle("/authenticate", http.HandlerFunc(this.handleAuthenticate))
	this.mux.Handle("/messages", http.HandlerFunc(this.handleMessages))
	this.mux.Handle("/pollUpdates", http.HandlerFunc(this.handlePollUpdates))
	this.mux.Handle("/sendMessage", http.HandlerFunc(this.handleSendMessage))
	this.mux.Handle("/markConversation", http.HandlerFunc(this.handleMarkConversation))
	this.mux.Handle("/attachment", http.HandlerFunc(this.handleFetchAttachment))
	this.mux.Handle("/uploadAttachment", http.HandlerFunc(this.handleUploadAttachment))
	this.mux.Handle("/updates", http.HandlerFunc(this.handleUpdates))

	this.mux.Handle("/", http.HandlerFunc(this.handleNotFound))

	return &this
}
