package frontend

import (
	"encoding/json"
	// "fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strconv"
	"sync"
	"time"

	"git.autistici.org/ale/djrandom/api"
	db_client "git.autistici.org/ale/djrandom/services/database/client"
	"github.com/gorilla/mux"
)

var (
	defaultConcurrency = 20
)

func sendJsonResponse(w http.ResponseWriter, resp interface{}) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// Upload receives files from sync clients, saves them to permanent
// storage and creates new Song objects in the db.
func Upload(w http.ResponseWriter, r *djRequest) {
	// Create a local temporary file, and copy the request body to
	// it. As soon as this is done, return an 'ok' response to the
	// client (and process the file in the background).
	tmpf, err := ioutil.TempFile("", "djrandom_upload_")
	if err != nil {
		log.Printf("Upload(): Error creating temporary file: %s", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	defer r.Request.Body.Close()

	_, err = io.Copy(tmpf, r.Request.Body)
	if err != nil {
		log.Printf("Upload(): Error saving file to local storage: %s", err)
		os.Remove(tmpf.Name())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	tmpf.Close()

	// Run further processing in the background.  AnalyzeAndStore
	// will remove the file when it's done.
	go AnalyzeAndStore(r.Ctx, tmpf.Name())

	sendJsonResponse(w, &api.UploadResponse{Ok: true})
}

// GetSongInfo returns data on a specific song.
func GetSongInfo(w http.ResponseWriter, r *djRequest) {
	vars := mux.Vars(r.Request)
	songId := vars["id"]
	log.Printf("GetSongInfo(%s)", songId)

	id, err := api.ParseSongID(songId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	song, ok := r.Ctx.Db.GetSongWithoutDupes(nil, id)
	if !ok {
		http.Error(w, "Not Found", http.StatusNotFound)
		return
	}

	sendJsonResponse(w, song)
}

// GetManySongsInfo returns data on many songs.
func GetManySongsInfo(w http.ResponseWriter, r *djRequest) {
	var songsReq api.GetManySongsRequest
	if err := json.NewDecoder(r.Request.Body).Decode(&songsReq); err != nil {
		log.Printf("GetManySongs(): Bad request: %s", err)
		http.Error(w, "Bad Request", http.StatusBadRequest)
		return
	}

	songs, err := db_client.ParallelFetchSongs(r.Ctx.Db, songsReq.SongIds)
	if err != nil {
		log.Printf("GetManySongs(): %s", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	resp := api.GetManySongsResponse{
		Results: songs,
	}
	sendJsonResponse(w, &resp)
}

// GetAlbumArt returns art for an album.
func GetAlbumArt(w http.ResponseWriter, r *djRequest) {
	args := r.Request.URL.Query()
	artist := args.Get("artist")
	album := args.Get("album")
	if artist == "" || album == "" {
		http.Error(w, "Not Found", http.StatusNotFound)
		return
	}

	// This will always return an image (maybe empty).
	img := r.Ctx.AlbumArt.GetAlbumArt(artist, album)
	w.Header().Set("Content-Type", "image/jpeg")
	w.Header().Set("Content-Length", strconv.Itoa(len(img)))
	expire := time.Now().Add(8760 * time.Hour)
	w.Header().Set("Expires", expire.Format(http.TimeFormat))
	w.Write(img)
}

// CheckFingerprints verifies if songs are already in the db.
func CheckFingerprints(w http.ResponseWriter, r *djRequest) {
	var fpReq api.FingerprintRequest
	if err := json.NewDecoder(r.Request.Body).Decode(&fpReq); err != nil {
		log.Printf("CheckFingerprints(): Bad request: %s", err)
		http.Error(w, "Bad Request", http.StatusBadRequest)
		return
	}

	// Run all the parallel queries within a single database session.
	s, err := r.Ctx.Db.NewSession()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer s.Close()

	// Type for replies.
	type fpResponse struct {
		fp   string
		dupe bool
	}

	// Check services/database/client/database.go for a
	// description of the concurrency pattern.
	rch := make(chan fpResponse)
	go func() {
		var wg sync.WaitGroup
		ch := make(chan bool, defaultConcurrency)
		for _, fp := range fpReq.Fingerprints {
			wg.Add(1)
			ch <- true
			go func(fp string) {
				defer func() { <-ch }()
				defer wg.Done()
				if _, ok := r.Ctx.Db.GetAudioFile(s, fp); ok {
					rch <- fpResponse{fp, true}
				} else {
					rch <- fpResponse{fp, false}
				}
			}(fp)
		}

		go func() {
			wg.Wait()
			close(ch)
			close(rch)
		}()
	}()

	// Collector.
	var fpResp api.FingerprintResponse
	for fpr := range rch {
		if fpr.dupe {
			fpResp.Dupes = append(fpResp.Dupes, fpr.fp)
		} else {
			fpResp.Missing = append(fpResp.Missing, fpr.fp)
		}
	}

	log.Printf("CheckFingerprints(): ok, %d/%d", len(fpResp.Dupes), len(fpResp.Missing))
	sendJsonResponse(w, &fpResp)
}

// SearchIds runs a search query and returns only song IDs.
func SearchIds(w http.ResponseWriter, r *djRequest) {
	// Assemble the search query.
	query := r.Request.FormValue("q")
	if query == "" {
		http.Error(w, "Empty search query", http.StatusBadRequest)
		return
	}

	// Run search.
	var resp api.SearchIdsResponse
	for item, _ := range r.Ctx.Index.Search(query) {
		resp.Results = append(resp.Results, item)
	}
	sendJsonResponse(w, &resp)
}

// Search returns full search results.
func Search(w http.ResponseWriter, r *djRequest) {
	// Assemble the search query.
	query := r.Request.FormValue("q")
	if query == "" {
		http.Error(w, "Empty search query", http.StatusBadRequest)
		return
	}

	// Run search (and create a list).
	results := r.Ctx.Index.Search(query)
	songIds := make([]api.SongID, 0, len(results))
	for id, _ := range results {
		songIds = append(songIds, id)
	}

	songs, err := db_client.ParallelFetchSongs(r.Ctx.Db, songIds)
	if err != nil {
		log.Printf("Search(): %s", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	resp := api.SearchResponse{
		Results: songs,
	}
	sendJsonResponse(w, &resp)
}

// ArtistAutocomplete returns artist autocompletion results.
func ArtistAutocomplete(w http.ResponseWriter, r *djRequest) {
	prefix := r.Request.URL.Query().Get("prefix")
	if prefix == "" {
		http.Error(w, "Empty prefix", http.StatusBadRequest)
		return
	}

	authors, err := r.Ctx.Db.GetArtists(nil, prefix)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	resp := struct {
		Entries []string `json:"entries"`
	}{authors}
	sendJsonResponse(w, &resp)
}

// AddPlayLog adds an entry to the play log.
func AddPlayLog(w http.ResponseWriter, r *djRequest) {
	var playLogReq api.AddPlayLogRequest
	if err := json.NewDecoder(r.Request.Body).Decode(&playLogReq); err != nil {
		log.Printf("AddPlayLog(): Bad request: %s", err)
		http.Error(w, "Bad Request", http.StatusBadRequest)
		return
	}

	entry := &api.PlayLogEntry{
		User:      r.AuthUser,
		Songs:     playLogReq.Songs,
		Timestamp: time.Now().Unix(),
	}
	if err := r.Ctx.Db.AppendPlayLog(nil, entry); err != nil {
		log.Printf("AddPlayLog(): %s", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	success := true
	sendJsonResponse(w, &success)
}

// USER API

func UserGetAuthKeys(w http.ResponseWriter, r *djRequest) {
	user, _ := r.Ctx.Db.GetUser(nil, r.AuthUser)
	sendJsonResponse(w, user.AuthKeyIds)
}

func UserCreateAuthKey(w http.ResponseWriter, r *djRequest) {
	s, err := r.Ctx.Db.NewSession()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer s.Close()

	user, _ := r.Ctx.Db.GetUser(s, r.AuthUser)
	authKey := user.NewAuthKey()
	err = r.Ctx.Db.PutAuthKey(s, authKey)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	sendJsonResponse(w, authKey)
}

func UserDeleteAuthKey(w http.ResponseWriter, r *djRequest) {
	s, err := r.Ctx.Db.NewSession()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer s.Close()

	user, _ := r.Ctx.Db.GetUser(s, r.AuthUser)
	keyId := r.Request.FormValue("auth_key_id")

	var newKeyIds []string
	for _, k := range user.AuthKeyIds {
		if k != keyId {
			newKeyIds = append(newKeyIds, k)
		}
	}
	user.AuthKeyIds = newKeyIds
	err = r.Ctx.Db.PutUser(s, user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.WriteHeader(200)
}