package canaconnect

import (
	"crypto/rand"
	_ "embed"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"math/big"
	"net/http"
	"strings"
	"sync"
	"time"

	"src.rybka.ca/pkg/sms"
	"src.rybka.ca/pkg/util"
	"src.rybka.ca/pkg/websocket"
)

//go:embed main.js
var mainJS []byte

//go:embed main.css
var mainCSS []byte

//go:embed home.html
var homeHTML string

//go:embed login.html
var loginHTML string

type Server struct {
	DB         *util.FileDB[Data]
	SMS        *sms.Client
	websockets map[string]chan []byte
	dataConns  map[string][]string
	callConns  map[string]string
	mu         sync.Mutex
}

func (s *Server) logError(err error) {
	fmt.Println("ERROR:", err)
}

func (s *Server) Handler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/admin", s.handleAdmin)
	mux.HandleFunc("/auth/login", s.handleLogin)
	mux.HandleFunc("/", s.handleHomeView)
	mux.HandleFunc("/main.js", s.handleMainJS)
	mux.HandleFunc("/main.css", s.handleMainCSS)
	mux.HandleFunc("PUT /settings/display-name", s.handleSetDisplayName)
	mux.HandleFunc("POST /contacts/{phone}/request", s.handleRequestContact)
	mux.HandleFunc("POST /contacts/{phone}/accept", s.handleAcceptContact)
	mux.HandleFunc("POST /call", s.handleCreateCall)
	mux.HandleFunc("DELETE /call", s.handleDeleteCall)
	mux.HandleFunc("GET /call", s.handleGetCall)
	mux.HandleFunc("PUT /call/description", s.handleSetCallDescription)
	mux.HandleFunc("POST /call/candidate", s.handleSetCallCandidate)
	// mux.HandleFunc("POST /contacts/{phone}/decline", s.handleDeclineContact)
	// mux.HandleFunc("POST /contacts/{phone}/block", s.handleBlockContact)
	// mux.HandleFunc("POST /contacts/{phone}/unblock", s.handleUnblockContact)
	return mux
}

func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		if userID != "4168928549" {
			http.Error(w, "not authorized", http.StatusUnauthorized)
			return
		}
		res := struct {
			Sockets   []string
			DataConns map[string][]string
			CallConns map[string]string
		}{
			DataConns: s.dataConns,
			CallConns: s.callConns,
		}
		for k := range s.websockets {
			res.Sockets = append(res.Sockets, k)
		}
		json.NewEncoder(w).Encode(res)
	})
}

func (s *Server) handleSetCallDescription(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		otherID := ""
		err := s.DB.Do(func(d *Data) error {
			otherID = d.Users[userID].ConnectedTo
			return nil
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if otherID == "" {
			http.Error(w, "no call", http.StatusBadRequest)
			return
		}
		socketID := s.callConns[otherID]
		data, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		b, err := json.Marshal(Message{
			Cmd:  "description",
			Args: []string{string(data)},
		})
		if err != nil {
			panic(err)
		}
		ok := s.messageWebsocket(socketID, b)
		if !ok {
			http.Error(w, "peer socket closed", http.StatusBadRequest)
			return
		}
	})
}

func (s *Server) handleSetCallCandidate(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		otherID := ""
		err := s.DB.Do(func(d *Data) error {
			otherID = d.Users[userID].ConnectedTo
			return nil
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if otherID == "" {
			http.Error(w, "no call", http.StatusBadRequest)
			return
		}
		socketID := s.callConns[otherID]
		data, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		b, err := json.Marshal(Message{
			Cmd:  "candidate",
			Args: []string{string(data)},
		})
		if err != nil {
			panic(err)
		}
		ok := s.messageWebsocket(socketID, b)
		if !ok {
			http.Error(w, "peer socket closed", http.StatusBadRequest)
			return
		}
	})
}

func (s *Server) handleGetCall(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		if !websocket.IsWebsocketRequest(r) {
			http.Error(w, "not websocket", http.StatusBadRequest)
			return
		}
		socketID, err := s.openWebsocket(w, r, func(id string) {
			s.mu.Lock()
			delete(s.callConns, userID)
			s.mu.Unlock()
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		s.closeWebsocket(s.callConns[userID])
		s.mu.Lock()
		if s.callConns == nil {
			s.callConns = map[string]string{}
		}
		s.callConns[userID] = socketID
		s.mu.Unlock()
	})
}

func (s *Server) handleCreateCall(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		params, err := getParams(r)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		calleeID := params["to"]

		// Check if callee is a contact
		isContact := false
		err = s.DB.Do(func(d *Data) error {
			for _, id := range d.Users[calleeID].Contacts {
				if id == userID {
					isContact = true
					break
				}
			}
			return nil
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if !isContact {
			http.Error(w, "not a contact", http.StatusBadRequest)
			return
		}

		// Check if busy
		isBusy := false
		err = s.DB.Do(func(d *Data) error {
			isBusy = d.Users[calleeID].RingFrom != "" ||
				d.Users[calleeID].ConnectedTo != "" ||
				d.Users[calleeID].Calling != ""
			return nil
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if isBusy {
			http.Error(w, "busy", http.StatusBadRequest)
			return
		}

		// Start call
		err = s.DB.Do(func(d *Data) error {
			d.Users[userID].Calling = calleeID
			d.Users[calleeID].RingFrom = userID
			return nil
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// TODO: Ring
	})
}

func (s *Server) authenticate(w http.ResponseWriter, r *http.Request, do func(userID string)) {
	id, _, ok, err := s.getUser(r)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if !ok {
		loginPage(w, r, "", nil, nil)
		return
	}
	do(id)
}

func (s *Server) openWebsocket(w http.ResponseWriter, r *http.Request, onClose func(id string)) (string, error) {
	conn, err := websocket.Upgrade(w, websocket.GetKey(r))
	if err != nil {
		return "", err
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	id := util.RandomString(16, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ1234567890")
	for {
		if _, ok := s.websockets[id]; !ok {
			break
		}
		id = util.RandomString(16, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ1234567890")
	}

	ch := make(chan []byte)
	if s.websockets == nil {
		s.websockets = map[string]chan []byte{}
	}
	s.websockets[id] = ch

	go func() {
		for {
			b, ok := <-ch
			if !ok {
				return
			}
			websocket.Write(conn, b)
		}
	}()

	go func() {
		websocket.Read(conn)
		close(ch)
		s.mu.Lock()
		delete(s.websockets, id)
		s.mu.Unlock()
		onClose(id)
	}()

	return id, nil
}

func (s *Server) messageWebsocket(id string, b []byte) bool {
	ch, ok := s.websockets[id]
	if !ok {
		return false
	}
	ch <- b
	return true
}

func (s *Server) closeWebsocket(id string) {
	ch, ok := s.websockets[id]
	if !ok {
		return
	}
	close(ch)
	s.mu.Lock()
	delete(s.websockets, id)
	s.mu.Unlock()
}

func (s *Server) callState(userID string) (string, error) {
	state := "available"
	err := s.DB.Do(func(d *Data) error {
		u := d.Users[userID]
		if u.RingFrom != "" {
			state = "incoming"
		} else if u.Calling != "" {
			state = "outgoing"
		} else if u.ConnectedTo != "" {
			state = "connected"
		}
		return nil
	})
	if err != nil {
		return "", err
	}
	return state, nil
}

func (s *Server) handleDeleteCall(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		callState, err := s.callState(userID)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		otherID := ""
		switch callState {
		case "incoming":
			err = s.DB.Do(func(d *Data) error {
				otherID = d.Users[userID].RingFrom
				d.Users[userID].RingFrom = ""
				d.Users[otherID].Calling = ""
				return nil
			})
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			socketID, ok := s.callConns[otherID]
			if ok {
				b, _ := json.Marshal(Message{Cmd: "end"})
				s.messageWebsocket(socketID, b)
			}
			s.updateClients(userID)
			s.updateClients(otherID)
		case "outgoing":
			err = s.DB.Do(func(d *Data) error {
				otherID = d.Users[userID].Calling
				d.Users[userID].Calling = ""
				d.Users[otherID].RingFrom = ""
				return nil
			})
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			socketID, ok := s.callConns[userID]
			if ok {
				b, _ := json.Marshal(Message{Cmd: "end"})
				s.messageWebsocket(socketID, b)
			}
			s.updateClients(userID)
			s.updateClients(otherID)
		case "connected":
			err = s.DB.Do(func(d *Data) error {
				otherID = d.Users[userID].ConnectedTo
				d.Users[userID].ConnectedTo = ""
				d.Users[otherID].ConnectedTo = ""
				return nil
			})
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			socketID, ok := s.callConns[userID]
			if ok {
				b, _ := json.Marshal(Message{Cmd: "end"})
				s.messageWebsocket(socketID, b)
			}
			socketID, ok = s.callConns[otherID]
			if ok {
				b, _ := json.Marshal(Message{Cmd: "end"})
				s.messageWebsocket(socketID, b)
			}
			s.updateClients(userID)
			s.updateClients(otherID)
		case "available":
			http.Error(w, "no call", http.StatusBadRequest)
		default:
			panic("unreachable")
		}
	})
}

func (s *Server) handleRequestContact(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		phone := r.PathValue("phone")
		err := s.DB.Do(func(d *Data) error {
			d.RequestContact(userID, phone)
			return nil
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		s.updateClients(phone)
	})
}

func (s *Server) handleAcceptContact(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		phone := r.PathValue("phone")
		err := s.DB.Do(func(d *Data) error {
			d.AcceptContact(userID, phone)
			return nil
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		s.updateClients(phone)
		s.updateClients(userID)
	})
}

func (s *Server) handleSetDisplayName(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		b, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		name := strings.TrimSpace(string(b))
		connections := []string{}
		err = s.DB.Do(func(d *Data) error {
			if d == nil {
				d = &Data{}
			}
			if d.Users == nil {
				d.Users = map[string]*User{}
			}
			if d.Users[userID] == nil {
				d.Users[userID] = &User{}
			}
			if d.Users[userID].Settings == nil {
				d.Users[userID].Settings = &Settings{}
			}
			d.Users[userID].Settings.DisplayName = name
			d.Users[userID].Settings.DisplayNameLastUpdated = time.Now().UnixMilli()

			connections = d.Connections(userID)

			return nil
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		s.updateClients(userID)
		for _, id := range connections {
			s.updateClients(id)
		}
	})
}

func (s *Server) handleMainCSS(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Content-Type", "text/css")
	w.Write(mainCSS)
}

func (s *Server) handleMainJS(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Content-Type", "text/javascript")
	w.Write(mainJS)
}

func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
	params, err := getParams(r)
	if err != nil {
		http.Error(w, "bad form", http.StatusBadRequest)
		return
	}
	phone := normalizePhone(params["phone"])
	err = validatePhone(phone)
	if err != nil {
		loginPage(w, r, phone, err, nil)
		return
	}
	code := params["code"]
	if code == "" {
		// Send login code
		err = s.sendLoginCode(phone)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Return login page
		loginPage(w, r, phone, nil, nil)
	} else {
		token, ok, err := s.login(phone, code)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if !ok {
			loginPage(w, r, phone, nil, fmt.Errorf("invalid code"))
			return
		}
		if strings.HasPrefix(r.Header.Get("Accept"), "text/html") {
			http.SetCookie(w, &http.Cookie{
				Name:   "token",
				Value:  token,
				Secure: true,
				Path:   "/",
			})
			http.Redirect(w, r, "/", http.StatusSeeOther)
		} else {
			w.Write([]byte(token))
		}
	}
}

func (s *Server) homeData(userID string) (*Home, error) {
	data := &Data{}
	err := s.DB.Do(func(d *Data) error {
		if d == nil {
			d = &Data{}
		}
		data = d
		return nil
	})
	if err != nil {
		return nil, err
	}
	return data.Home(userID), nil
}

func (s *Server) newClientID() string {
	for {
		id := util.RandomString(16, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ1234567890")
		if _, ok := s.websockets[id]; !ok {
			return id
		}
	}
}

func (s *Server) updateClients(userID string) {
	if s.dataConns == nil {
		return
	}
	for _, connID := range s.dataConns[userID] {
		home, err := s.homeData(userID)
		if err != nil {
			return
		}
		b, err := json.Marshal(home)
		if err != nil {
			panic(err)
		}
		s.messageWebsocket(connID, b)
	}
}

func (s *Server) addDataConn(userID string, connID string) {
	s.mu.Lock()
	if s.dataConns == nil {
		s.dataConns = map[string][]string{}
	}
	s.dataConns[userID] = append(s.dataConns[userID], connID)
	s.mu.Unlock()
}

func (s *Server) removeDataConn(userID string, connID string) {
	s.mu.Lock()
	for i, id := range s.dataConns[userID] {
		if id == connID {
			s.dataConns[userID] = append(s.dataConns[userID][:i], s.dataConns[userID][i+1:]...)
		}
	}
	s.mu.Unlock()
}

func (s *Server) handleHomeView(w http.ResponseWriter, r *http.Request) {
	s.authenticate(w, r, func(userID string) {
		home, err := s.homeData(userID)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		b, err := json.Marshal(home)
		if err != nil {
			panic(err)
		}
		if websocket.IsWebsocketRequest(r) {
			connID, err := s.openWebsocket(w, r, func(id string) {
				s.removeDataConn(userID, id)
			})
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			s.addDataConn(userID, connID)
		} else if strings.HasPrefix(r.Header.Get("Accept"), "text/html") {
			err = template.Must(template.New("home.html").Parse(homeHTML)).Execute(w, string(b))
		} else {
			_, err = w.Write(b)
		}
		if err != nil {
			panic(err)
		}
	})
}

func loginPage(w http.ResponseWriter, r *http.Request, phone string, phoneErr, codeErr error) {
	if strings.HasPrefix(r.Header.Get("Accept"), "text/html") {
		err := template.Must(template.New("login.html").Parse(loginHTML)).Execute(w, struct {
			Phone      string
			PhoneError error
			CodeError  error
		}{
			Phone:      phone,
			PhoneError: phoneErr,
			CodeError:  codeErr,
		})
		if err != nil {
			panic(err)
		}
		return
	}
	if phoneErr != nil {
		http.Error(w, phoneErr.Error(), http.StatusBadRequest)
	} else if codeErr != nil {
		http.Error(w, codeErr.Error(), http.StatusBadRequest)
	}
}

func (s *Server) getToken(r *http.Request) (string, bool) {
	authHeader := r.Header.Get("Authorization")
	if authHeader != "" {
		parts := strings.Split(authHeader, " ")
		if len(parts) != 2 {
			return "", false
		}
		return parts[1], true
	}
	c, err := r.Cookie("token")
	if err != nil {
		return "", false
	}
	return c.Value, true
}

func (s *Server) getUser(r *http.Request) (string, string, bool, error) {
	token, ok := s.getToken(r)
	if !ok {
		return "", "", false, nil
	}
	phone := ""
	err := s.DB.Do(func(d *Data) error {
		if d == nil {
			d = &Data{}
		}
		if d.Sessions == nil {
			d.Sessions = map[string]string{}
		}
		phone = d.Sessions[token]
		return nil
	})
	if err != nil {
		return "", "", false, err
	}
	if phone == "" {
		return "", "", false, nil
	}
	return phone, token, true, nil
}

func (s *Server) sendLoginCode(phone string) error {
	code, err := generateLoginCode()
	if err != nil {
		return err
	}
	err = s.DB.Do(func(d *Data) error {
		if d == nil {
			d = &Data{}
		}
		if d.Users == nil {
			d.Users = map[string]*User{}
		}
		if d.Users[phone] == nil {
			d.Users[phone] = &User{
				LoginCodes: map[string]bool{},
				Settings:   &Settings{},
				Contacts:   []string{},
				Requests:   []string{},
				Blocks:     map[string]bool{},
			}
		}
		d.Users[phone].LoginCodes[code] = true
		return nil
	})
	if err != nil {
		return err
	}

	go func() {
		time.Sleep(5 * time.Minute)
		err = s.DB.Do(func(d *Data) error {
			if d == nil {
				d = &Data{}
			}
			if d.Users == nil {
				d.Users = map[string]*User{}
			}
			if d.Users[phone] == nil {
				d.Users[phone] = &User{}
			}
			if d.Users[phone].LoginCodes == nil {
				d.Users[phone].LoginCodes = map[string]bool{}
			}
			delete(d.Users[phone].LoginCodes, code)
			return nil
		})
		if err != nil {
			s.logError(err)
		}
	}()

	msg := fmt.Sprintf("%s is your login code.", code)
	return s.SMS.Send(phone, msg)
}

func (s *Server) login(phone, code string) (string, bool, error) {
	// Check if code is valid
	var ok bool
	err := s.DB.Do(func(d *Data) error {
		if d == nil {
			ok = false
			return nil
		}
		if d.Users == nil {
			ok = false
			return nil
		}
		if d.Users[phone] == nil {
			ok = false
			return nil
		}
		if d.Users[phone].LoginCodes == nil {
			ok = false
			return nil
		}
		ok = d.Users[phone].LoginCodes[code]
		return nil
	})
	if err != nil {
		return "", false, err
	}
	if !ok {
		return "", false, nil
	}

	// Generate a session token
	token := ""
	err = s.DB.Do(func(d *Data) error {
		if d.Sessions == nil {
			d.Sessions = map[string]string{}
		}
		for {
			token, err = randomToken()
			if err != nil {
				return err
			}
			_, ok := d.Sessions[token]
			if !ok {
				break
			}
		}
		d.Sessions[token] = phone
		return nil
	})
	if err != nil {
		return "", false, err
	}

	// Deactivate the login code
	err = s.DB.Do(func(d *Data) error {
		if d == nil {
			d = &Data{}
		}
		if d.Users == nil {
			return nil
		}
		if d.Users[phone] == nil {
			d.Users[phone] = &User{}
		}
		if d.Users[phone].LoginCodes == nil {
			return nil
		}
		delete(d.Users[phone].LoginCodes, code)
		return nil
	})
	if err != nil {
		s.logError(err)
	}

	// Return the session token
	return token, true, nil
}

func generateLoginCode() (string, error) {
	n, err := rand.Int(rand.Reader, big.NewInt(1_000_000))
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%06d", n.Int64()), nil
}

func normalizePhone(phone string) string {
	res := util.StripNonDigits(phone)
	if len(res) == 11 && res[0] == '1' {
		res = strings.TrimPrefix(res, "1")
	}
	return res
}

func validatePhone(phone string) error {
	if len(phone) < 10 {
		return fmt.Errorf("too short: %s", phone)
	}
	if len(phone) > 10 {
		return fmt.Errorf("too long")
	}
	areaCode := phone[:3]
	if !util.CanadianAreaCodes[areaCode] {
		return fmt.Errorf("not a canadian number")
	}
	return nil
}

func randomToken() (string, error) {
	const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	b := make([]byte, 64)
	_, err := rand.Read(b)
	if err != nil {
		return "", err
	}
	out := make([]byte, 64)
	for i := range b {
		out[i] = chars[int(b[i])%len(chars)]
	}
	return string(out), nil
}

func getParams(r *http.Request) (map[string]string, error) {
	res := map[string]string{}
	if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
		err := r.ParseForm()
		if err != nil {
			return nil, err
		}
		for k, v := range r.Form {
			if len(v) != 1 {
				return nil, fmt.Errorf("key %s has %d values", k, len(v))
			}
			res[k] = v[0]
		}
		return res, nil
	}
	err := json.NewDecoder(r.Body).Decode(&res)
	if err != nil {
		return nil, err
	}
	return res, nil
}
