package main

import (
	"database/sql"
	"embed"
	"encoding/json"
	"flag"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"net/url"
	"regexp"
	"slices"
	"strings"
	"sync"

	"github.com/gorilla/mux"
	_ "github.com/ncruces/go-sqlite3/driver"
	_ "github.com/ncruces/go-sqlite3/embed"
)

var port = flag.Int("port", 8080, "Port to listen on")
var apiPort = flag.Int("api-port", 8081, "Port to listen on for API requests")
var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")

//go:embed memberships-tmpl/*
var tmpls embed.FS

//go:embed static/*
var staticResources embed.FS

type cachingHandler struct {
	h http.Handler
}

func (h cachingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Cache-Control", "max-age=604800")
	h.h.ServeHTTP(w, r)
}

type Server struct {
	store         Store
	syncAddresses map[string]struct{}
	mu            sync.Mutex
}

type Group struct {
	Id          string `json:"id"`
	Title       string `json:"title"`
	Description string `json:"description"`
}

type User struct {
	Id       string `json:"id"`
	Username string `json:"username"`
	Email    string `json:"email"`
}

func getLoggedInUser(r *http.Request) (string, error) {
	if user := r.Header.Get("X-Forwarded-UserId"); user != "" {
		return user, nil
	} else {
		return "", fmt.Errorf("unauthenticated")
	}
	// return "0063f4b6-29cb-4bd6-b8ce-1f6b203d490f", nil
}

type Status int

const (
	Owner Status = iota
	Member
)

func (s *Server) Start() error {
	e := make(chan error)
	go func() {
		r := mux.NewRouter()
		r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticResources))})
		r.HandleFunc("/group/{groupId:.*}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
		r.HandleFunc("/group/{groupId:.*}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
		r.HandleFunc("/group/{groupId:.*}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
		r.HandleFunc("/group/{groupId:.*}/remove-child-group/{otherId:.*}", s.removeChildGroupHandler).Methods(http.MethodPost)
		r.HandleFunc("/group/{groupId:.*}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost)
		r.HandleFunc("/group/{groupId:.*}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost)
		r.HandleFunc("/group/{groupId:.*}", s.groupHandler)
		r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost)
		r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost)
		r.HandleFunc("/user/{username}", s.userHandler)
		r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost)
		r.HandleFunc("/", s.homePageHandler)
		e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
	}()
	go func() {
		r := mux.NewRouter()
		r.HandleFunc("/api/init", s.apiInitHandler)
		// TODO(gio): change to /api/users/{username}
		r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost)
		r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
		r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
		r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
		r.HandleFunc("/api/group", s.apiCreateGroup).Methods(http.MethodPost)
		r.HandleFunc("/api/add-group-user", s.apiAddUserToGroup).Methods(http.MethodPost)
		r.HandleFunc("/api/remove-group-user", s.apiRemoveUserFromGroup).Methods(http.MethodPost)
		r.HandleFunc("/api/add-group-group", s.apiAddGroupToGroup).Methods(http.MethodPost)
		r.HandleFunc("/api/remove-group-group", s.apiRemoveGroupFromGroup).Methods(http.MethodPost)
		r.HandleFunc("/api/get-group", s.apiGetGroup).Methods(http.MethodPost)
		e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
	}()
	return <-e
}

type GroupData struct {
	Group      Group
	Membership string
}

func (s *Server) checkIsOwner(w http.ResponseWriter, userId, groupId string) error {
	ownerUsers, err := s.store.GetOwnerUsers(nil, groupId)
	if err != nil {
		return err
	}
	if slices.ContainsFunc(ownerUsers, func(u User) bool {
		return u.Id == userId
	}) {
		return nil
	}
	ownerIds, err := s.store.GetOwnerGroups(nil, groupId)
	if err != nil {
		return err
	}
	canActAs, err := s.store.GetGroupsUserCanActAs(nil, userId)
	if err != nil {
		return err
	}
	for _, g := range canActAs {
		if slices.Index(ownerIds, g) != -1 {
			return nil
		}
	}
	return fmt.Errorf("not an owner")
}

type templates struct {
	group *template.Template
	user  *template.Template
}

func parseTemplates(fs embed.FS) (templates, error) {
	base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
	if err != nil {
		return templates{}, err
	}
	parse := func(path string) (*template.Template, error) {
		if b, err := base.Clone(); err != nil {
			return nil, err
		} else {
			return b.ParseFS(fs, path)
		}
	}
	user, err := parse("memberships-tmpl/user.html")
	if err != nil {
		return templates{}, err
	}
	group, err := parse("memberships-tmpl/group.html")
	if err != nil {
		return templates{}, err
	}
	return templates{group, user}, nil
}

func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
}

type UserPageData struct {
	User             User
	OwnerGroups      []Group
	MembershipGroups []Group
	TransitiveGroups []Group
	LoggedInUserPage bool
	SSHPublicKeys    []string
	ErrorMessage     string
}

func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	errorMsg := r.URL.Query().Get("errorMessage")
	vars := mux.Vars(r)
	user := strings.ToLower(vars["username"])
	// TODO(dtabidze): should check if username exists or not.
	loggedInUserPage := loggedInUser == user
	ownerGroups, err := s.store.GetGroupsUserOwns(nil, user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	membershipGroups, err := s.store.GetGroupsUserIsMemberOf(nil, user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	transitiveGroups, err := s.store.GetGroupsUserCanActAs(nil, user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	userInfo, err := s.store.GetUser(nil, user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	sshPublicKeys, err := s.store.GetUserPublicKeys(nil, user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	data := UserPageData{
		User:             userInfo,
		OwnerGroups:      ownerGroups,
		MembershipGroups: membershipGroups,
		TransitiveGroups: transitiveGroups,
		LoggedInUserPage: loggedInUserPage,
		SSHPublicKeys:    sshPublicKeys,
		ErrorMessage:     errorMsg,
	}
	templates, err := parseTemplates(tmpls)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if err := templates.user.Execute(w, data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	var group Group
	group.Id = r.PostFormValue("id")
	group.Title = r.PostFormValue("title")
	group.Description = r.PostFormValue("description")
	if err := isValidGroupId(group.Id); err != nil {
		// http.Error(w, err.Error(), http.StatusBadRequest)
		redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	if err := s.store.CreateGroup(nil, loggedInUser, group); err != nil {
		// http.Error(w, err.Error(), http.StatusInternalServerError)
		redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

type GroupPageData struct {
	GroupId          string
	Title            string
	Description      string
	Owners           []User
	Members          []User
	AllGroups        []Group
	AllUsers         []User
	TransitiveGroups []Group
	ChildGroups      []Group
	OwnerGroups      []Group
	ErrorMessage     string
}

func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
	_, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	errorMsg := r.URL.Query().Get("errorMessage")
	vars := mux.Vars(r)
	groupId := normalizeGroupId(vars["groupId"])
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	g, err := s.store.GetGroup(nil, groupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	owners, err := s.store.GetOwnerUsers(nil, groupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	members, err := s.store.GetMemberUsers(nil, groupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	allUsers, err := s.store.GetAllUsers(nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	allGroups, err := s.store.GetAllGroups(nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	transitiveGroups, err := s.store.GetGroupsGroupCanActAs(nil, groupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	childGroups, err := s.store.GetMemberGroups(nil, groupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	ownerGroups, err := s.store.GetOwnerGroups(nil, groupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	data := GroupPageData{
		GroupId:          groupId,
		Title:            g.Title,
		Description:      g.Description,
		Owners:           owners,
		Members:          members,
		AllGroups:        allGroups,
		AllUsers:         allUsers,
		TransitiveGroups: transitiveGroups,
		ChildGroups:      childGroups,
		OwnerGroups:      ownerGroups,
		ErrorMessage:     errorMsg,
	}
	templates, err := parseTemplates(tmpls)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if err := templates.group.Execute(w, data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	groupId := normalizeGroupId(vars["groupId"])
	otherId := vars["otherId"]
	if err := isValidGroupId(groupId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := isValidGroupId(otherId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return
	}
	err = s.store.RemoveMemberGroup(nil, groupId, otherId)
	if err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}

func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	username := vars["username"]
	groupId := normalizeGroupId(vars["groupId"])
	if err := isValidGroupId(groupId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return
	}
	err = s.store.RemoveOwnerUser(nil, groupId, username)
	if err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}

func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	username := vars["username"]
	groupId := normalizeGroupId(vars["groupId"])
	if err := isValidGroupId(groupId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return
	}
	err = s.store.RemoveMemberUser(nil, groupId, username)
	if err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}

func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	groupId := normalizeGroupId(vars["groupId"])
	if err := isValidGroupId(groupId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	userId := strings.ToLower(r.FormValue("userId"))
	if userId == "" {
		http.Error(w, "Username parameter is required", http.StatusBadRequest)
		return
	}
	status, err := convertStatus(r.FormValue("status"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return
	}
	switch status {
	case Owner:
		err = s.store.AddOwnerUser(nil, groupId, userId)
	case Member:
		err = s.store.AddMemberUser(nil, groupId, userId)
	default:
		http.Error(w, "Invalid status", http.StatusBadRequest)
		return
	}
	if err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}

func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
	// TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	groupId := normalizeGroupId(vars["groupId"])
	otherId := r.FormValue("otherId")
	if err := isValidGroupId(groupId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := isValidGroupId(otherId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return
	}
	if err := s.store.AddMemberGroup(nil, groupId, otherId); err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}

func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	groupId := normalizeGroupId(vars["groupId"])
	otherId := r.FormValue("otherId")
	if err := isValidGroupId(groupId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := isValidGroupId(otherId); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return
	}
	if err := s.store.AddOwnerGroup(nil, groupId, otherId); err != nil {
		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}

func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
	defer s.pingAllSyncAddresses()
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	username := vars["username"]
	if loggedInUser != username {
		http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
		return
	}
	sshKey := r.FormValue("ssh-key")
	if sshKey == "" {
		http.Error(w, "SSH key not present", http.StatusBadRequest)
		return
	}
	if err := s.store.AddUserPublicKey(nil, strings.ToLower(username), sshKey); err != nil {
		redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
}

func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
	defer s.pingAllSyncAddresses()
	loggedInUser, err := getLoggedInUser(r)
	if err != nil {
		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
		return
	}
	vars := mux.Vars(r)
	username := vars["username"]
	if loggedInUser != username {
		http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}
	sshKey := r.FormValue("ssh-key")
	if sshKey == "" {
		http.Error(w, "SSH key not present", http.StatusBadRequest)
		return
	}
	if err := s.store.RemoveUserPublicKey(nil, username, sshKey); err != nil {
		redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
		http.Redirect(w, r, redirectURL, http.StatusFound)
		return
	}
	http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
}

type initRequest struct {
	User   User    `json:"user"`
	Groups []Group `json:"groups"`
}

func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
	var req initRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := s.store.Init(req.User, req.Groups); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

type userInfo struct {
	CanActAs []Group `json:"canActAs"`
	OwnerOf  []Group `json:"ownerOf"`
}

func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	user, ok := vars["username"]
	if !ok || user == "" {
		http.Error(w, "Username parameter is required", http.StatusBadRequest)
		return
	}
	user = strings.ToLower(user)
	transitiveGroups, err := s.store.GetGroupsUserCanActAs(nil, user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	owned, err := s.store.GetGroupsUserOwns(nil, user)
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(userInfo{transitiveGroups, owned}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
	s.addSyncAddress(r.FormValue("selfAddress"))
	var users []User
	var err error
	groups := r.FormValue("groups")
	if groups == "" {
		users, err = s.store.GetAllUsers(nil)
	} else {
		uniqueUsers := make(map[string]struct{})
		g := strings.Split(groups, ",")
		uniqueTG := make(map[string]struct{})
		for _, group := range g {
			uniqueTG[group] = struct{}{}
			trGroups, err := s.store.GetGroupsGroupCanActAs(nil, group)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			for _, tg := range trGroups {
				uniqueTG[tg.Title] = struct{}{}
			}
		}
		for group := range uniqueTG {
			u, err := s.store.GetMemberUsers(nil, group)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			for _, user := range u {
				uniqueUsers[user.Username] = struct{}{}
			}
		}
		usernames := make([]string, 0, len(uniqueUsers))
		for username := range uniqueUsers {
			usernames = append(usernames, username)
		}
		users, err = s.store.GetUsers(nil, usernames)
	}
	if err != nil {
		http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(users); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
	defer s.pingAllSyncAddresses()
	var req User
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}
	if req.Id == "" {
		http.Error(w, "Id cannot be empty", http.StatusBadRequest)
		return
	}
	if req.Username == "" {
		http.Error(w, "Username cannot be empty", http.StatusBadRequest)
		return
	}
	if req.Email == "" {
		http.Error(w, "Email cannot be empty", http.StatusBadRequest)
		return
	}
	user := User{Id: req.Id, Username: strings.ToLower(req.Username), Email: strings.ToLower(req.Email)}
	if err := s.store.CreateUser(nil, user); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

type createGroupRequest struct {
	UserId      string `json:"userId"`
	GroupId     string `json:"groupId"`
	Title       string `json:"title"`
	Description string `json:"description"`
}

func (s *Server) apiCreateGroup(w http.ResponseWriter, r *http.Request) {
	var req createGroupRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}
	if err := s.store.CreateGroup(nil, req.UserId, Group{req.GroupId, req.Title, req.Description}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

type apiGetGroupReq struct {
	GroupId string `json:"groupId"`
}

// TODO(gio): update client
type apiGetGroupResp struct {
	Self         Group   `json:"self"`
	Members      []User  `json:"members"`
	Owners       []User  `json:"owners"`
	MemberGroups []Group `json:"memberGroups"`
	OwnerGroups  []Group `json:"ownerGroups"`
}

func (s *Server) apiGetGroup(w http.ResponseWriter, r *http.Request) {
	var req apiGetGroupReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "missing group name", http.StatusBadRequest)
		return
	}
	group, err := s.store.GetGroup(nil, req.GroupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	members, err := s.store.GetMemberUsers(nil, req.GroupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	owners, err := s.store.GetOwnerUsers(nil, req.GroupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	memberGroups, err := s.store.GetMemberGroups(nil, req.GroupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	ownerGroups, err := s.store.GetOwnerGroups(nil, req.GroupId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if err := json.NewEncoder(w).Encode(apiGetGroupResp{group, members, owners, memberGroups, ownerGroups}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

type userGroupStatus struct {
	GroupId string `json:"groupId"`
	UserId  string `json:"userId"`
	Status  string `json:"status"`
}

func (s *Server) apiAddUserToGroup(w http.ResponseWriter, r *http.Request) {
	var req userGroupStatus
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}
	status, err := convertStatus(req.Status)
	if err != nil {
		http.Error(w, "Invalid status", http.StatusBadRequest)
		return
	}
	fmt.Printf("AAA %+v\n", req)
	switch status {
	case Member:
		err = s.store.AddMemberUser(nil, req.GroupId, req.UserId)
	case Owner:
		err = s.store.AddOwnerUser(nil, req.GroupId, req.UserId)
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func (s *Server) apiRemoveUserFromGroup(w http.ResponseWriter, r *http.Request) {
	var req userGroupStatus
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}
	status, err := convertStatus(req.Status)
	if err != nil {
		http.Error(w, "Invalid status", http.StatusBadRequest)
		return
	}
	switch status {
	case Member:
		err = s.store.RemoveMemberUser(nil, req.GroupId, req.UserId)
	case Owner:
		err = s.store.RemoveOwnerUser(nil, req.GroupId, req.UserId)
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

type groupGroupStatus struct {
	GroupId string `json:"groupId"`
	OtherId string `json:"otherId"`
	Status  string `json:"status"`
}

func (s *Server) apiAddGroupToGroup(w http.ResponseWriter, r *http.Request) {
	var req groupGroupStatus
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}
	status, err := convertStatus(req.Status)
	if err != nil {
		http.Error(w, "Invalid status", http.StatusBadRequest)
		return
	}
	switch status {
	case Member:
		err = s.store.AddMemberGroup(nil, req.GroupId, req.OtherId)
	case Owner:
		err = s.store.AddOwnerGroup(nil, req.GroupId, req.OtherId)
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func (s *Server) apiRemoveGroupFromGroup(w http.ResponseWriter, r *http.Request) {
	var req groupGroupStatus
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}
	status, err := convertStatus(req.Status)
	if err != nil {
		http.Error(w, "Invalid status", http.StatusBadRequest)
		return
	}
	switch status {
	case Member:
		err = s.store.RemoveMemberGroup(nil, req.GroupId, req.OtherId)
	case Owner:
		err = s.store.RemoveOwnerGroup(nil, req.GroupId, req.OtherId)
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

type getGroupsOwnedByRequest struct {
	User string `json:"user"`
}

type getGroupsOwnedByResponse struct {
	Groups []string `json:"groups"`
}

func (s *Server) apiGetUserOwnedGroups(w http.ResponseWriter, r *http.Request) {
	var req getGroupsOwnedByRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	groups, err := s.store.GetGroupsUserOwns(nil, req.User)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	var resp getGroupsOwnedByResponse
	for _, g := range groups {
		resp.Groups = append(resp.Groups, g.Title)
	}
	if err := json.NewEncoder(w).Encode(resp); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

type addUserKeyRequest struct {
	User      string `json:"user"`
	PublicKey string `json:"publicKey"`
}

func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) {
	var req addUserKeyRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}
	if req.User == "" {
		http.Error(w, "Username cannot be empty", http.StatusBadRequest)
		return
	}
	if req.PublicKey == "" {
		http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest)
		return
	}
	if err := s.store.AddUserPublicKey(nil, strings.ToLower(req.User), req.PublicKey); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

// TODO(gio): enque sync event instead of directly reaching out to clients.
// This will allow to deduplicate sync events and save resources.
func (s *Server) pingAllSyncAddresses() {
	s.mu.Lock()
	defer s.mu.Unlock()
	for address := range s.syncAddresses {
		go func(address string) {
			log.Printf("Pinging %s", address)
			resp, err := http.Get(address)
			if err != nil {
				// TODO(gio): remove sync address after N number of failures.
				log.Printf("Failed to ping %s: %v", address, err)
				return
			}
			defer resp.Body.Close()
			if resp.StatusCode != http.StatusOK {
				log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
			}
		}(address)
	}
}

func (s *Server) addSyncAddress(address string) {
	if address == "" {
		return
	}
	fmt.Printf("Adding sync address: %s\n", address)
	s.mu.Lock()
	defer s.mu.Unlock()
	s.syncAddresses[address] = struct{}{}
}

func convertStatus(status string) (Status, error) {
	switch strings.ToLower(status) {
	case "owner":
		return Owner, nil
	case "member":
		return Member, nil
	default:
		return Owner, fmt.Errorf("invalid status: %s", status)
	}
}

func isValidGroupId(group string) error {
	if strings.TrimSpace(group) == "" {
		return fmt.Errorf("Group name can't be empty or contain only whitespaces")
	}
	validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
	if !validGroupName.MatchString(group) {
		return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
	}
	return nil
}

func main() {
	flag.Parse()
	db, err := sql.Open("sqlite3", *dbPath)
	if err != nil {
		panic(err)
	}
	store, err := NewSQLiteStore(db)
	if err != nil {
		panic(err)
	}
	s := Server{
		store:         store,
		syncAddresses: make(map[string]struct{}),
		mu:            sync.Mutex{},
	}
	log.Fatal(s.Start())
}

func normalizeGroupId(groupId string) string {
	if strings.Contains(groupId, "/") && !strings.HasPrefix(groupId, "/") {
		return fmt.Sprintf("/%s", groupId)
	}
	return groupId
}
