package main

import (
	"crypto/rand"
	"embed"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/fs"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"sync"
	"time"
)

//go:embed static
var staticFS embed.FS

// ---------- Models ----------

type Option struct {
	ID   string `json:"id"`
	Date string `json:"date"`           // YYYY-MM-DD
	Time string `json:"time,omitempty"` // HH:MM, optional
}

type Vote struct {
	Name      string    `json:"name"`
	OptionIDs []string  `json:"optionIds"`
	CreatedAt time.Time `json:"createdAt"`
}

type Poll struct {
	ID          string    `json:"id"`
	AdminToken  string    `json:"-"` // never serialized to clients
	Title       string    `json:"title"`
	Description string    `json:"description,omitempty"`
	Options     []Option  `json:"options"`
	Votes       []Vote    `json:"votes"`
	Closed      bool      `json:"closed"`
	CreatedAt   time.Time `json:"createdAt"`
}

// pollFile is the on-disk representation (includes the admin token).
type pollFile struct {
	Poll
	AdminToken string `json:"adminToken"`
}

// ---------- Store ----------

type Store struct {
	mu    sync.Mutex
	path  string
	polls map[string]*Poll
}

func NewStore(path string) (*Store, error) {
	s := &Store{path: path, polls: map[string]*Poll{}}
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return s, nil
		}
		return nil, err
	}
	var list []pollFile
	if err := json.Unmarshal(data, &list); err != nil {
		return nil, fmt.Errorf("parsing %s: %w", path, err)
	}
	for i := range list {
		p := list[i].Poll
		p.AdminToken = list[i].AdminToken
		s.polls[p.ID] = &p
	}
	return s, nil
}

// persist writes all polls to disk. Caller must hold s.mu.
func (s *Store) persist() error {
	list := make([]pollFile, 0, len(s.polls))
	for _, p := range s.polls {
		list = append(list, pollFile{Poll: *p, AdminToken: p.AdminToken})
	}
	sort.Slice(list, func(i, j int) bool { return list[i].CreatedAt.Before(list[j].CreatedAt) })
	data, err := json.MarshalIndent(list, "", "  ")
	if err != nil {
		return err
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o644); err != nil {
		return err
	}
	return os.Rename(tmp, s.path)
}

func (s *Store) Create(p *Poll) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.polls[p.ID] = p
	return s.persist()
}

func (s *Store) Get(id string) (*Poll, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	p, ok := s.polls[id]
	if !ok {
		return nil, false
	}
	cp := *p // shallow copy is fine for read-only use
	return &cp, true
}

// Update runs fn on the poll under lock and persists the result.
func (s *Store) Update(id string, fn func(*Poll) error) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	p, ok := s.polls[id]
	if !ok {
		return errNotFound
	}
	if err := fn(p); err != nil {
		return err
	}
	return s.persist()
}

func (s *Store) Delete(id string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.polls[id]; !ok {
		return errNotFound
	}
	delete(s.polls, id)
	return s.persist()
}

var errNotFound = errors.New("not found")

// ---------- Helpers ----------

const idAlphabet = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"

func randomID(n int) string {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		panic(err)
	}
	out := make([]byte, n)
	for i, v := range b {
		out[i] = idAlphabet[int(v)%len(idAlphabet)]
	}
	return string(out)
}

var (
	dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
	timeRe = regexp.MustCompile(`^([01]\d|2[0-3]):[0-5]\d$`)
)

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, msg string) {
	writeJSON(w, status, map[string]string{"error": msg})
}

func adminToken(r *http.Request) string {
	if t := r.Header.Get("X-Admin-Token"); t != "" {
		return t
	}
	return r.URL.Query().Get("admin")
}

// ---------- Handlers ----------

type server struct {
	store *Store
}

type createPollRequest struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	Options     []struct {
		Date string `json:"date"`
		Time string `json:"time"`
	} `json:"options"`
}

func (sv *server) createPoll(w http.ResponseWriter, r *http.Request) {
	var req createPollRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "Invalid request body.")
		return
	}
	req.Title = strings.TrimSpace(req.Title)
	req.Description = strings.TrimSpace(req.Description)
	if req.Title == "" {
		writeError(w, http.StatusBadRequest, "A title is required.")
		return
	}
	if len(req.Title) > 120 || len(req.Description) > 600 {
		writeError(w, http.StatusBadRequest, "Title or description is too long.")
		return
	}
	if len(req.Options) == 0 {
		writeError(w, http.StatusBadRequest, "Select at least one date.")
		return
	}
	if len(req.Options) > 60 {
		writeError(w, http.StatusBadRequest, "Too many dates (max 60).")
		return
	}

	seen := map[string]bool{}
	options := make([]Option, 0, len(req.Options))
	for _, o := range req.Options {
		if !dateRe.MatchString(o.Date) {
			writeError(w, http.StatusBadRequest, "Invalid date: "+o.Date)
			return
		}
		if _, err := time.Parse("2006-01-02", o.Date); err != nil {
			writeError(w, http.StatusBadRequest, "Invalid date: "+o.Date)
			return
		}
		if o.Time != "" && !timeRe.MatchString(o.Time) {
			writeError(w, http.StatusBadRequest, "Invalid time for "+o.Date+" (use HH:MM).")
			return
		}
		key := o.Date + "T" + o.Time
		if seen[key] {
			continue
		}
		seen[key] = true
		options = append(options, Option{ID: randomID(8), Date: o.Date, Time: o.Time})
	}
	sort.Slice(options, func(i, j int) bool {
		if options[i].Date != options[j].Date {
			return options[i].Date < options[j].Date
		}
		return options[i].Time < options[j].Time
	})

	p := &Poll{
		ID:          randomID(10),
		AdminToken:  randomID(24),
		Title:       req.Title,
		Description: req.Description,
		Options:     options,
		Votes:       []Vote{},
		CreatedAt:   time.Now().UTC(),
	}
	if err := sv.store.Create(p); err != nil {
		log.Printf("create poll: %v", err)
		writeError(w, http.StatusInternalServerError, "Could not save the poll.")
		return
	}
	writeJSON(w, http.StatusCreated, map[string]string{"id": p.ID, "adminToken": p.AdminToken})
}

func (sv *server) getPoll(w http.ResponseWriter, r *http.Request) {
	p, ok := sv.store.Get(r.PathValue("id"))
	if !ok {
		writeError(w, http.StatusNotFound, "Poll not found.")
		return
	}
	resp := struct {
		*Poll
		IsAdmin bool `json:"isAdmin"`
	}{p, adminToken(r) == p.AdminToken}
	writeJSON(w, http.StatusOK, resp)
}

type voteRequest struct {
	Name      string   `json:"name"`
	OptionIDs []string `json:"optionIds"`
}

func (sv *server) vote(w http.ResponseWriter, r *http.Request) {
	var req voteRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "Invalid request body.")
		return
	}
	req.Name = strings.TrimSpace(req.Name)
	if req.Name == "" {
		writeError(w, http.StatusBadRequest, "Enter your name so friends know who answered.")
		return
	}
	if len(req.Name) > 60 {
		writeError(w, http.StatusBadRequest, "Name is too long.")
		return
	}

	err := sv.store.Update(r.PathValue("id"), func(p *Poll) error {
		if p.Closed {
			return errors.New("This poll is closed.")
		}
		valid := map[string]bool{}
		for _, o := range p.Options {
			valid[o.ID] = true
		}
		ids := []string{}
		seen := map[string]bool{}
		for _, id := range req.OptionIDs {
			if valid[id] && !seen[id] {
				ids = append(ids, id)
				seen[id] = true
			}
		}
		v := Vote{Name: req.Name, OptionIDs: ids, CreatedAt: time.Now().UTC()}
		// Same name (case-insensitive) replaces the earlier answer.
		for i := range p.Votes {
			if strings.EqualFold(p.Votes[i].Name, req.Name) {
				p.Votes[i] = v
				return nil
			}
		}
		p.Votes = append(p.Votes, v)
		return nil
	})
	if err != nil {
		if errors.Is(err, errNotFound) {
			writeError(w, http.StatusNotFound, "Poll not found.")
		} else {
			writeError(w, http.StatusConflict, err.Error())
		}
		return
	}
	writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}

func (sv *server) withAdmin(fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		p, ok := sv.store.Get(r.PathValue("id"))
		if !ok {
			writeError(w, http.StatusNotFound, "Poll not found.")
			return
		}
		if adminToken(r) != p.AdminToken {
			writeError(w, http.StatusForbidden, "Only the poll creator can do this. Use your admin link.")
			return
		}
		fn(w, r)
	}
}

func (sv *server) closePoll(w http.ResponseWriter, r *http.Request) {
	err := sv.store.Update(r.PathValue("id"), func(p *Poll) error {
		p.Closed = true
		return nil
	})
	if err != nil {
		writeError(w, http.StatusNotFound, "Poll not found.")
		return
	}
	writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}

func (sv *server) deletePoll(w http.ResponseWriter, r *http.Request) {
	if err := sv.store.Delete(r.PathValue("id")); err != nil {
		writeError(w, http.StatusNotFound, "Poll not found.")
		return
	}
	writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}

// ---------- main ----------

func main() {
	addr := flag.String("addr", ":8080", "listen address")
	dataDir := flag.String("data", "data", "directory for poll storage")
	flag.Parse()

	if err := os.MkdirAll(*dataDir, 0o755); err != nil {
		log.Fatal(err)
	}
	store, err := NewStore(filepath.Join(*dataDir, "polls.json"))
	if err != nil {
		log.Fatal(err)
	}
	sv := &server{store: store}

	static, _ := fs.Sub(staticFS, "static")
	mux := http.NewServeMux()

	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFileFS(w, r, static, "index.html")
	})
	mux.HandleFunc("GET /p/{id}", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFileFS(w, r, static, "poll.html")
	})
	mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(static)))

	mux.HandleFunc("POST /api/polls", sv.createPoll)
	mux.HandleFunc("GET /api/polls/{id}", sv.getPoll)
	mux.HandleFunc("POST /api/polls/{id}/votes", sv.vote)
	mux.HandleFunc("POST /api/polls/{id}/close", sv.withAdmin(sv.closePoll))
	mux.HandleFunc("DELETE /api/polls/{id}", sv.withAdmin(sv.deletePoll))

	log.Printf("mediator running on http://localhost%s (data in %s)", *addr, *dataDir)
	log.Fatal(http.ListenAndServe(*addr, mux))
}
