package api

import (
	"crypto/rand"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"math/big"
	"net/http"
	"strings"
	"sync"

	"src.rybka.ca/pkg/apps/phone"
	"src.rybka.ca/pkg/apps/settings"
	"src.rybka.ca/pkg/sms"
	"src.rybka.ca/pkg/storage"
	"src.rybka.ca/pkg/util"
)

type Server struct {
	Storage   *storage.LocalClient
	SMS       *sms.Client
	loginLock sync.Mutex
	callers   map[string]*WebConnection
}

func (s *Server) authenticate(w http.ResponseWriter, r *http.Request, h func(u *User)) {
	authHeader := r.Header.Get("Authorization")
	authToken := strings.TrimPrefix(authHeader, "Bearer ")
	if authToken == "" {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	b, err := s.Storage.ReadFile(fmt.Sprintf("auth/sessions/%s/account_id", authToken))
	if authToken == "" {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	accountID := string(b)
	if accountID == "" {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	u := &User{
		AccountID: accountID,
		db:        s.Storage,
	}

	h(u)
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	mux := http.NewServeMux()
	mux.HandleFunc("OPTIONS /auth/send-login-code", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, OPTIONS")
		w.WriteHeader(204)
	})
	mux.HandleFunc("POST /auth/send-login-code", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, OPTIONS")

		req := &SendLoginCodeRequest{}
		err := json.NewDecoder(r.Body).Decode(req)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		err = validatePhone(normalizePhone(req.Phone))
		if err != nil {
			http.Error(w, fmt.Sprintf("invalid phone: %s", err), http.StatusBadRequest)
			return
		}
		code, err := generateLoginCode()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		err = s.Storage.WriteFile(fmt.Sprintf("auth/login_codes/%s_%s/active", req.Phone, code), "true")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		msg := fmt.Sprintf("%s is your login code.", code)
		err = s.SMS.Send(req.Phone, msg)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		res := SendLoginCodeResponse{}
		json.NewEncoder(w).Encode(res)
	})
	mux.HandleFunc("OPTIONS /auth/login", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, OPTIONS")
		w.WriteHeader(204)
	})
	mux.HandleFunc("POST /auth/login", func(w http.ResponseWriter, r *http.Request) {
		s.loginLock.Lock()
		defer s.loginLock.Unlock()

		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, OPTIONS")

		// Parse request
		req := &LoginRequest{}
		err := json.NewDecoder(r.Body).Decode(req)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		// Check if code is valid
		codePath := fmt.Sprintf("auth/login_codes/%s_%s/active", req.Phone, req.Code)
		b, err := s.Storage.ReadFile(codePath)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if string(b) != "true" {
			http.Error(w, "invalid code", http.StatusBadRequest)
			return
		}

		// Generate a session token
		token := ""
		for {
			token, err = randomToken()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			// Check it doesn't already exist
			b, err := s.Storage.ReadFile(fmt.Sprintf("auth/sessions/%s/account_id", token))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			if len(b) == 0 {
				break
			}
		}

		// Save the token
		err = s.Storage.WriteFile(fmt.Sprintf("auth/sessions/%s/account_id", token), req.Phone)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Deactivate the login code
		err = s.Storage.Delete(fmt.Sprintf("auth/login_codes/%s_%s", req.Phone, req.Code))
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Return the session token
		json.NewEncoder(w).Encode(LoginResponse{
			SessionToken: token,
		})
	})
	mux.HandleFunc("OPTIONS /auth/logout", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		w.WriteHeader(204)
	})
	mux.HandleFunc("POST /auth/logout", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")

		s.authenticate(w, r, func(u *User) {
			err := u.Logout()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	})
	mux.HandleFunc("OPTIONS /settings", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		w.WriteHeader(204)
	})
	mux.HandleFunc("GET /settings", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")

		s.authenticate(w, r, func(u *User) {
			res, err := u.Settings()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			err = json.NewEncoder(w).Encode(res)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	})
	mux.HandleFunc("PUT /settings", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")

		s.authenticate(w, r, func(u *User) {
			d := settings.Data{}
			err := json.NewDecoder(r.Body).Decode(&d)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}

			err = u.SetSettingsDisplayName(d.DisplayName)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	})
	mux.HandleFunc("OPTIONS /people", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		w.WriteHeader(204)
	})
	mux.HandleFunc("GET /people", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")

		s.authenticate(w, r, func(u *User) {
			res, err := u.People()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			err = json.NewEncoder(w).Encode(res)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	})
	mux.HandleFunc("OPTIONS /phone/call/from", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		w.WriteHeader(204)
	})
	mux.HandleFunc("GET /phone/call/from", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		s.authenticate(w, r, func(u *User) {
			caller, err := s.Storage.ReadFile(fmt.Sprintf("%s/phone/call/from", u.AccountID))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			err = json.NewEncoder(w).Encode(caller)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	})
	mux.HandleFunc("OPTIONS /phone/call/offer", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		w.WriteHeader(204)
	})
	mux.HandleFunc("GET /phone/call/offer", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		s.authenticate(w, r, func(u *User) {
			offer, err := s.Storage.ReadFile(fmt.Sprintf("%s/phone/call/offer", u.AccountID))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			err = json.NewEncoder(w).Encode(offer)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	})
	mux.HandleFunc("OPTIONS /phone/call", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		w.WriteHeader(204)
	})
	mux.HandleFunc("GET /phone/call", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		s.authenticate(w, r, func(u *User) {
			res := &phone.Call{}
			from, err := s.Storage.ReadFile(fmt.Sprintf("%s/phone/call/from", u.AccountID))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			offer, err := s.Storage.ReadFile(fmt.Sprintf("%s/phone/call/offer", u.AccountID))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			res.From = from
			res.Offer = offer
			err = json.NewEncoder(w).Encode(res)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	})
	mux.HandleFunc("OPTIONS /phone/call/decline", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		w.WriteHeader(204)
	})
	mux.HandleFunc("POST /phone/call/decline", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Header().Add("Access-Control-Allow-Methods", "POST, OPTIONS")
		w.Header().Add("Access-Control-Allow-Headers", "Authorization")
		s.authenticate(w, r, func(u *User) {
			caller, err := s.Storage.ReadFile(fmt.Sprintf("%s/phone/call/from", u.AccountID))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			s.callers[caller].WriteMsg("end")
			err = s.Storage.Delete(fmt.Sprintf("%s/phone/call/from", u.AccountID))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	})
	mux.HandleFunc("/phone/call/dial", func(w http.ResponseWriter, r *http.Request) {
		authToken := r.URL.Query().Get("token")
		if authToken == "" {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}

		b, err := s.Storage.ReadFile(fmt.Sprintf("auth/sessions/%s/account_id", authToken))
		if authToken == "" {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		accountID := string(b)
		if accountID == "" {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}

		if r.Header.Get("Connection") == "" || r.Header.Get("Upgrade") != "websocket" {
			http.Error(w, "WebSocket required", http.StatusBadRequest)
			return
		}

		h, ok := w.(http.Hijacker)
		if !ok {
			http.Error(w, "Cannot hijack", http.StatusInternalServerError)
			return
		}

		conn, buf, err := h.Hijack()
		if err != nil {
			return
		}

		key := r.Header.Get("Sec-WebSocket-Key")
		accept := computeAcceptKey(key)

		// WebSocket handshake response
		fmt.Fprintf(buf,
			"HTTP/1.1 101 Switching Protocols\r\n"+
				"Upgrade: websocket\r\n"+
				"Connection: Upgrade\r\n"+
				"Sec-WebSocket-Accept: %s\r\n\r\n", accept)
		buf.Flush()

		ws := &WebConnection{rw: conn}

		if s.callers == nil {
			s.callers = map[string]*WebConnection{}
		}
		s.callers[accountID] = ws

		to := r.URL.Query().Get("to")
		if to == "" {
			ws.WriteMsg("error", "no to")
			return
		}

		// Check if friends

		// Check if busy

		// Set incoming call
		err = s.Storage.WriteFile(fmt.Sprintf("%s/phone/call/from", to), accountID)
		if err != nil {
			ws.WriteMsg("error", err.Error())
			return
		}
		defer func() {
			s.Storage.Delete(fmt.Sprintf("%s/phone/call/from", to))
			if to == "4168928549" {
				http.Post("http://100.75.248.92:3333/stop", "", nil)
			}
		}()

		// Ring target
		if to == "4168928549" {
			res, err := http.Post("http://100.75.248.92:3333/play/youtube?id=q-sTeDy9obg", "", nil)
			if err != nil {
				fmt.Println("error", err.Error())
				b, err := json.Marshal(Msg{
					Fn:   "error",
					Args: []string{err.Error()},
				})
				if err != nil {
					panic(err)
				}
				ws.WriteText(string(b))
			}
			if res.StatusCode != 200 {
				ws.WriteMsg("error", res.Status)
				return
			}
		}

		// Read loop
		for {
			m, err := ws.ReadMsg()
			if err != nil {
				ws.WriteMsg("error", err.Error())
				return
			}
			switch m.Fn {
			case "answer":
				s.callers[to].WriteMsg("answer", m.Args[0])
			case "ice":
				s.callers[to].WriteMsg("ice", m.Args[0])
			case "end":
				s.callers[to].WriteMsg("end")
			default:
				ws.WriteMsg("yo", "hey")
			}
		}
	})
	mux.HandleFunc("/phone/call/accept", func(w http.ResponseWriter, r *http.Request) {
		authToken := r.URL.Query().Get("token")
		if authToken == "" {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}

		b, err := s.Storage.ReadFile(fmt.Sprintf("auth/sessions/%s/account_id", authToken))
		if authToken == "" {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		accountID := string(b)
		if accountID == "" {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}

		if r.Header.Get("Connection") == "" || r.Header.Get("Upgrade") != "websocket" {
			http.Error(w, "WebSocket required", http.StatusBadRequest)
			return
		}

		h, ok := w.(http.Hijacker)
		if !ok {
			http.Error(w, "Cannot hijack", http.StatusInternalServerError)
			return
		}

		conn, buf, err := h.Hijack()
		if err != nil {
			return
		}

		key := r.Header.Get("Sec-WebSocket-Key")
		accept := computeAcceptKey(key)

		// WebSocket handshake response
		fmt.Fprintf(buf,
			"HTTP/1.1 101 Switching Protocols\r\n"+
				"Upgrade: websocket\r\n"+
				"Connection: Upgrade\r\n"+
				"Sec-WebSocket-Accept: %s\r\n\r\n", accept)
		buf.Flush()

		ws := &WebConnection{rw: conn}

		if s.callers == nil {
			s.callers = map[string]*WebConnection{}
		}
		s.callers[accountID] = ws

		caller, err := s.Storage.ReadFile(fmt.Sprintf("%s/phone/call/from", accountID))
		if err != nil {
			ws.WriteMsg("error", err.Error())
			return
		}
		if caller == "" {
			ws.WriteMsg("end")
			return
		}

		// Read loop
		for {
			m, err := ws.ReadMsg()
			if err != nil {
				ws.WriteMsg("error", err.Error())
				return
			}
			switch m.Fn {
			case "offer":
				s.callers[caller].WriteMsg("offer", m.Args[0])
			case "ice":
				s.callers[caller].WriteMsg("ice", m.Args[0])
			case "end":
				s.callers[caller].WriteMsg("end")
			default:
				ws.WriteMsg("yo", "hey")
			}
		}
	})
	mux.ServeHTTP(w, r)
}

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 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")
	}
	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 computeAcceptKey(key string) string {
	const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
	h := sha1.Sum([]byte(key + magic))
	return base64.StdEncoding.EncodeToString(h[:])
}
