package main

import (
	"compress/gzip"
	"database/sql"
	"encoding/json"
	"fmt"
	_ "github.com/mattn/go-sqlite3"
	"io"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

type Database struct {
	db *sql.DB
}

func TimezoneToLocation(timezone string) *time.Location {
	defaultLocation := time.FixedZone("UTC", 0)
	location, err := time.LoadLocation(timezone)
	if err != nil {
		return defaultLocation
	} else {
		return location
	}
}

func TimeStringToTime(timeString string) time.Time {
	t, _ := time.Parse("2006-01-02 15:04:05", timeString)
	return t
}

func TimeStringToTimeInLocation(timeString string, timezone string) string {
	t := TimeStringToTime(timeString)
	location := TimezoneToLocation(timezone)
	return t.In(location).Format("2006-01-02 15:04:05")
}

func OpenDatabase(filepath string) *Database {
	if db, err := sql.Open("sqlite3", filepath); err != nil {
		return nil
	} else {
		return &Database{
			db: db,
		}
	}
}

func (db *Database) Close() {
	db.db.Close()
}

func (db *Database) DbCreateTableMessages() {
	stmt := `CREATE TABLE IF NOT EXISTS messages (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		ip_address TEXT NOT NULL,
		content TEXT NOT NULL,
		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
	)`
	db.db.Exec(stmt)
}

func (db *Database) DbCreateTableUsers() {
	stmt := `CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		ip_address TEXT NOT NULL,
		username TEXT NOT NULL UNIQUE,
		timezone TEXT DEFAULT 'America/New_York',
		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
	)`
	db.db.Exec(stmt)
}

func (db *Database) UserTimezoneSet(ip_address, timezone string) {
	_, err := db.db.Exec("UPDATE users SET timezone = ? WHERE ip_address = ?", timezone, ip_address)
	if err != nil {
		fmt.Println(err)
	}
}

func (db *Database) UserAdd(ip_address, username string) {
	_, err := db.db.Exec("INSERT INTO users (username, ip_address) VALUES (?, ?)", username, ip_address)
	if err != nil {
		fmt.Println(err)
	}
}

func (db *Database) MessageAdd(ip_address string, content string) {
	_, err := db.db.Exec("INSERT INTO messages (ip_address, content) VALUES (?, ?)", ip_address, content)
	if err != nil {
		fmt.Println(err)
	}
}

func (db *Database) UserNameGet(ip_address string) string {
	rows, err := db.db.Query("SELECT username FROM users WHERE ip_address = ?", ip_address)
	if err != nil {
		fmt.Println(err)
	}
	defer rows.Close()
	var username string
	rows.Next()
	rows.Scan(&username)
	return username
}

func (db *Database) UserIpGet(username string) string {
	rows, err := db.db.Query("SELECT ip_address FROM users WHERE username = ?", username)
	if err != nil {
		fmt.Println(err)
	}
	defer rows.Close()
	var ip_address string
	rows.Next()
	rows.Scan(&ip_address)
	return ip_address
}

func (db *Database) UserGetTimezone(ip_address string) string {
	rows, err := db.db.Query("SELECT timezone FROM users WHERE ip_address = ?", ip_address)
	if err != nil {
		fmt.Println(err)
	}
	defer rows.Close()
	var timezone string
	rows.Next()
	rows.Scan(&timezone)
	return timezone
}

func (db *Database) UsersGet() []User {
	rows, err := db.db.Query("SELECT * FROM users")
	if err != nil {
		fmt.Println(err)
	}
	defer rows.Close()
	var users []User
	for rows.Next() {
		var id string
		var ip_address string
		var username string
		var created_at string
		var timezone string
		rows.Scan(&id, &ip_address, &username, &created_at, &timezone)
		user := User{
			Id:        id,
			Username:  username,
			IpAddress: ip_address,
			Timezone:  timezone,
		}
		users = append(users, user)
	}
	return users
}

func (db *Database) MessagesGet() []Message {
	rows, err := db.db.Query(`
		SELECT messages.id, messages.ip_address, messages.content, 
		strftime('%Y-%m-%d %H:%M:%S', messages.created_at) as created_at, 
		users.username
		FROM messages 
		LEFT JOIN users ON messages.ip_address = users.ip_address;
	`)
	if err != nil {
		fmt.Println(err)
	}
	defer rows.Close()

	var messages []Message

	for rows.Next() {
		var id string
		var content string
		var ip_address string
		var created_at string
		var username string
		rows.Scan(&id, &ip_address, &content, &created_at, &username)

		message := Message{
			Id:             id,
			Content:        content,
			SenderIp:       ip_address,
			SenderUsername: username,
			Timestamp:      created_at,
		}

		messages = append(messages, message)
	}
	return messages
}

func (db *Database) UserNameExists(username string) bool {
	rows, err := db.db.Query("SELECT * FROM users WHERE username = ?", username)
	if err != nil {
		fmt.Println(err)
	}
	defer rows.Close()
	return rows.Next()
}

func (db *Database) UserExists(ip string) bool {
	rows, err := db.db.Query("SELECT * FROM users WHERE ip_address = ?", ip)
	if err != nil {
		fmt.Println(err)
	}
	defer rows.Close()
	return rows.Next()
}

func (db *Database) UserNameChange(ip, newUsername string) {
	_, err := db.db.Exec("UPDATE users SET username = ? WHERE ip_address = ?", newUsername, ip)
	if err != nil {
		fmt.Println(err)
	}
}

func (db *Database) UserMessagesDelete(ip string) {
	_, err := db.db.Exec("DELETE FROM messages WHERE ip_address = ?", ip)
	if err != nil {
		fmt.Println(err)
	}
}

func (db *Database) UserMessagesGet(ip string) []Message {
	rows, err := db.db.Query(`
		SELECT messages.*, users.username 
		FROM messages 
		LEFT JOIN users ON messages.ip_address = users.ip_address 
		WHERE messages.ip_address = ?
		ORDER BY messages.created_at DESC;
	`, ip)
	if err != nil {
		fmt.Println(err)
	}
	defer rows.Close()

	var messages []Message

	for rows.Next() {
		var id string
		var content string
		var ip_address string
		var created_at string
		var username string
		rows.Scan(&id, &ip_address, &content, &created_at, &username)
		t, _ := time.Parse(created_at, created_at)
		message := Message{
			Id:             id,
			Content:        content,
			SenderIp:       ip_address,
			SenderUsername: username,
			Timestamp:      t.Format(created_at),
		}
		messages = append(messages, message)
	}
	return messages
}

func (db *Database) MessageDeleteId(id string) {
	_, err := db.db.Exec("DELETE FROM messages WHERE id = ?", id)
	if err != nil {
		fmt.Println(err)
	}
}
func (db *Database) MessageDeleteIfOwner(id string, ip string) (int, error) {
	res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND ip_address = ?", id, ip)
	if err != nil {
		return 0, err
	}
	affected, err := res.RowsAffected()
	if err != nil {
		return 0, err
	}
	return int(affected), nil

}

func (db *Database) DeleteOldMessages(ageMinutes int) {
	if ageMinutes <= 0 {
		return
	}
	age := strconv.Itoa(ageMinutes)
	_, err := db.db.Exec("DELETE FROM messages WHERE created_at < datetime('now', ? || ' minutes')", "-"+age)
	if err != nil {
		fmt.Println(err)
	}
}

func (db *Database) UserDeleteIp(ip string) {
	_, err := db.db.Exec("DELETE FROM users WHERE ip_address = ?", ip)
	if err != nil {
		fmt.Println(err)
	}
}

func (db *Database) UsersDelete() {
	_, err := db.db.Exec("DELETE FROM users")
	if err != nil {
		fmt.Println(err)
	}
}

func (db *Database) MessagesDelete() {
	_, err := db.db.Exec("DELETE FROM messages")
	if err != nil {
		fmt.Println(err)
	}
}

type gzipResponseWriter struct {
	http.ResponseWriter
	io.Writer
}

func (g *gzipResponseWriter) Write(data []byte) (int, error) {
	return g.Writer.Write(data)
}

func GzipMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
			next.ServeHTTP(w, r)
			return
		}

		gz := gzip.NewWriter(w)
		defer gz.Close()

		w.Header().Set("Content-Encoding", "gzip")
		next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
	})
}

func getClientIP(r *http.Request) string {
	if fwdIP := r.Header.Get("X-Forwarded-For"); fwdIP != "" {
		return strings.Split(fwdIP, ",")[0]
	}

	clientIP := r.RemoteAddr
	if host, _, err := net.SplitHostPort(clientIP); err == nil {
		return host
	}
	return clientIP
}

type User struct {
	Id        string
	Username  string
	IpAddress string
	Timezone  string
}

type Message struct {
	Id             string
	Content        string
	SenderIp       string
	SenderUsername string
	Timestamp      string
}

type Server struct {
	Connected map[string]time.Time // Map IP -> Last activity time
	Database  *Database
	Config    Config
	mu        sync.Mutex // For thread safety
}

func NewServer(config Config) *Server {

	return &Server{
		Connected: make(map[string]time.Time),
		Database:  OpenDatabase(config.Paths.DatabasePath),
		Config:    config,
		mu:        sync.Mutex{},
	}
}

func (s *Server) AddMessage(userip string, contents string) {
	s.Database.MessageAdd(userip, contents)
}

func (s *Server) updateActivity(ip string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.Connected[ip] = time.Now()
}

func (s *Server) cleanupActivity() {
	s.mu.Lock()
	defer s.mu.Unlock()
	for ip, lastActivity := range s.Connected {
		if time.Since(lastActivity) > 10*time.Second {
			delete(s.Connected, ip)
		}
	}
}

func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
	clientIP := getClientIP(r)
	s.updateActivity(clientIP)
	s.cleanupActivity()
	w.WriteHeader(http.StatusOK)
}

func validUsername(username string) bool {
	for _, c := range username {
		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
			return false
		}
	}
	return true
}

func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPut {
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}

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

	clientIP := getClientIP(r)

	var req struct {
		Username string `json:"username"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
		return
	}

	s.mu.Lock()

	if len(req.Username) > 64 {
		http.Error(w, fmt.Sprintf(`{"error": "Username too long (must be less than 64 characters)"}`), http.StatusRequestEntityTooLarge)
		s.mu.Unlock()
		return
	}

	if !validUsername(req.Username) {
		http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
		s.mu.Unlock()
		return
	}

	if s.Database.UserNameExists(req.Username) {
		s.mu.Unlock()
		http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
		return
	}

	if s.Database.UserExists(clientIP) {
		s.Database.UserNameChange(clientIP, req.Username)
	} else {
		s.Database.UserAdd(clientIP, req.Username)
	}

	s.mu.Unlock()

	json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"})
}

func getMessageTemplate(filepath string, body string) string {
	contents, _ := os.ReadFile(filepath)
	return strings.Replace(string(contents), "{{body}}", body, 1)
}

func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		w.Header().Set("Content-Type", "text/html")

		var body string
		messages := s.Database.MessagesGet()
		for _, msg := range messages {
			clientIP := getClientIP(r)
			timeZone := s.Database.UserGetTimezone(clientIP)
			timeLocal := TimeStringToTimeInLocation(msg.Timestamp, timeZone)
			body += fmt.Sprintf(`<p>%s<br><span class="username">%s</span><br><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
				msg.Id, msg.SenderUsername, timeLocal, msg.Content)
		}

		w.Write([]byte(getMessageTemplate(s.Config.Paths.MessagesHtmlPath, body)))
	case http.MethodPut:
		w.Header().Set("Content-Type", "application/json")

		// Get client's IP
		clientIP := getClientIP(r)

		s.mu.Lock()
		exists := s.Database.UserExists(clientIP)
		username := s.Database.UserNameGet(clientIP)
		s.mu.Unlock()

		if !exists {
			errorFmt := fmt.Sprintf(`{"error": "IP %s not registered with username"}`, clientIP)
			http.Error(w, errorFmt, http.StatusUnauthorized)
			return
		}

		var msg struct {
			Message string `json:"message"`
		}

		if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
			http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
			return
		}

		s.Database.MessageAdd(clientIP, msg.Message)

		json.NewEncoder(w).Encode(map[string]string{
			"status": "Message received",
			"from":   username,
		})
	case http.MethodDelete:
		w.Header().Set("Content-Type", "application/json")

		var req struct {
			MessageId string `json:"messageId"`
		}

		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
			return
		}

		clientIP := getClientIP(r)
		if affected, err := s.Database.MessageDeleteIfOwner(req.MessageId, clientIP); err != nil {
			http.Error(w, `{"error": "Unauthorized"}`, http.StatusNotFound)
			return
		} else if affected == 0 {
			http.Error(w, `{"error": "Message not found"}`, http.StatusNotFound)
			return
		}

		json.NewEncoder(w).Encode(map[string]string{
			"status": "Message deleted",
		})
	default:
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}
}

func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	clientIP := getClientIP(r)

	s.mu.Lock()
	exists := s.Database.UserExists(clientIP)
	username := s.Database.UserNameGet(clientIP)
	s.mu.Unlock()

	json.NewEncoder(w).Encode(map[string]interface{}{
		"hasUsername": exists,
		"username":    username,
	})
}

func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}

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

	clientIP := getClientIP(r)
	s.updateActivity(clientIP)
	s.cleanupActivity()
	s.mu.Lock()
	var users []string
	for ip := range s.Connected {
		// for all connected, get their usernames
		users = append(users, s.Database.UserNameGet(ip))
	}
	s.mu.Unlock()

	json.NewEncoder(w).Encode(map[string]interface{}{
		"users": users,
	})
}

func (s *Server) handleTimezone(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}
	w.Header().Set("Content-Type", "application/json")

	clientIP := getClientIP(r)

	var req struct {
		Timezone string `json:"timezone"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
		return
	}

	s.mu.Lock()

	if !s.Database.UserExists(clientIP) {
		http.Error(w, `{"error": "User not registered"}`, http.StatusUnauthorized)
		s.mu.Unlock()
		return
	}

	s.Database.UserTimezoneSet(clientIP, req.Timezone)
	s.mu.Unlock()
	json.NewEncoder(w).Encode(map[string]string{
		"status": "success",
	})
}

func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", "text/html")
	file := readFile(s.Config.Paths.IndexHtmlPath)
	w.Write(file)
}

func readFile(filepath string) []byte {
	contents, _ := os.ReadFile(filepath)
	return contents
}

func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) {
	_ = r
	w.Header().Set("Content-Type", "application/javascript")
	file := readFile(s.Config.Paths.IndexJsPath)
	w.Write(file)
}

func (s *Server) handleCss(w http.ResponseWriter, r *http.Request) {
	_ = r
	w.Header().Set("Content-Type", "text/css")
	file := readFile(s.Config.Paths.IndexCssPath)
	w.Write(file)
}

func (s *Server) Run() {
	s.Database.DbCreateTableMessages()
	s.Database.DbCreateTableUsers()
	s.Database.DeleteOldMessages(s.Config.Server.MessageMaxAge)
	handler := http.NewServeMux()
	handler.HandleFunc("/ping", s.handlePing)
	handler.HandleFunc("/username", s.handleUsername)
	handler.HandleFunc("/messages", s.handleMessages)
	handler.HandleFunc("/username/status", s.handleUsernameStatus)
	handler.HandleFunc("/users", s.handleUsers)
	handler.HandleFunc("/", s.handleRoot)
	handler.HandleFunc("/root.js", s.handleJs)
	handler.HandleFunc("/root.css", s.handleCss)
	handler.HandleFunc("/timezone", s.handleTimezone)
	fmt.Printf("Server starting on %s:%d\n", s.Config.Server.IpAddress, s.Config.Server.Port)
	defer s.Stop()
	if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Config.Server.IpAddress, s.Config.Server.Port), GzipMiddleware(handler)); err != nil {
		fmt.Printf("Server error: %v\n", err)
	}
}

func (s *Server) Stop() {
	s.Database.Close()
}

func main() {
	if len(os.Args) < 2 {
		fmt.Printf("Usage: %s <config.json>\n", os.Args[0])
		os.Exit(1)
	}
	_, err := os.OpenFile(os.Args[1], os.O_RDONLY, 0)
	if err != nil {
		fmt.Println("Error opening config file: ", err)
		os.Exit(1)
	}
	config := LoadConfig(os.Args[1])
	fmt.Println("Config loaded")
	server := NewServer(config)
	server.Run()
}

type Config struct {
	Server struct {
		IpAddress     string `json:"ipAddress"`
		Port          int    `json:"port"`
		MessageMaxAge int    `json:"messageMaxAge"`
	} `json:"server"`
	Paths struct {
		DatabasePath     string `json:"databasePath"`
		IndexJsPath      string `json:"indexJsPath"`
		IndexCssPath     string `json:"indexCssPath"`
		IndexHtmlPath    string `json:"indexHtmlPath"`
		MessagesHtmlPath string `json:"messagesHtmlPath"`
	} `json:"paths"`
}

func LoadConfig(filepath string) Config {
	contents, _ := os.ReadFile(filepath)
	var config Config
	err := json.Unmarshal(contents, &config)
	config.Paths.IndexHtmlPath = pathMaker(config.Paths.IndexHtmlPath)
	config.Paths.IndexJsPath = pathMaker(config.Paths.IndexJsPath)
	config.Paths.IndexCssPath = pathMaker(config.Paths.IndexCssPath)
	config.Paths.MessagesHtmlPath = pathMaker(config.Paths.MessagesHtmlPath)
	config.Paths.DatabasePath = pathMaker(config.Paths.DatabasePath)
	if err != nil {
		fmt.Println("Error parsing config file: ", err)
		os.Exit(1)
	}
	return config
}

func pathMaker(path string) string {
	absPath, _ := filepath.Abs(path)
	absPath = filepath.Clean(absPath)
	fmt.Println(absPath)
	return absPath
}
