package server

import (
	"context"
	"io"
	"net/http"
	"path/filepath"

	"git.netflux.io/rob/clipper/config"
	"git.netflux.io/rob/clipper/filestore"
	"git.netflux.io/rob/clipper/media"
	"github.com/google/uuid"
	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
	"github.com/gorilla/schema"
	"github.com/improbable-eng/grpc-web/go/grpcweb"
	"go.uber.org/zap"
)

type httpHandler struct {
	*mux.Router

	grpcHandler     *grpcweb.WrappedGrpcServer
	mediaSetService MediaSetService
	logger          *zap.SugaredLogger
}

func newHTTPHandler(grpcHandler *grpcweb.WrappedGrpcServer, mediaSetService MediaSetService, c config.Config, logger *zap.SugaredLogger) *httpHandler {
	fileStoreHandler := http.NotFoundHandler()
	if c.FileStoreHTTPRoot != "" {
		logger.With("root", c.FileStoreHTTPRoot, "baseURL", c.FileStoreHTTPBaseURL.String()).Info("Configured to serve file store over HTTP")
		fileStoreHandler = http.FileServer(&indexedFileSystem{http.Dir(c.FileStoreHTTPRoot)})
	}

	assetsHandler := http.NotFoundHandler()
	if c.AssetsHTTPRoot != "" {
		logger.With("root", c.AssetsHTTPRoot).Info("Configured to serve assets over HTTP")
		assetsHandler = http.FileServer(&indexedFileSystem{http.Dir(c.AssetsHTTPRoot)})
	}

	// If FileSystemStore AND assets serving are both enabled,
	// FileStoreHTTPBaseURL *must* be set to a value other than "/" to avoid
	// clobbering the assets routes.
	h := &httpHandler{
		Router:          mux.NewRouter(),
		grpcHandler:     grpcHandler,
		mediaSetService: mediaSetService,
		logger:          logger,
	}

	h.
		Methods("POST").
		Path("/api/media_sets/{id}/clip").
		Handler(http.HandlerFunc(h.handleClip))

	if c.FileStore == config.FileSystemStore {
		h.
			Methods("GET").
			PathPrefix(c.FileStoreHTTPBaseURL.Path).
			Handler(filestore.NewFileSystemStoreHTTPMiddleware(c.FileStoreHTTPBaseURL, fileStoreHandler))
	}

	h.
		Methods("GET").
		Handler(assetsHandler)

	h.Use(handlers.CORS(handlers.AllowedOrigins(c.CORSAllowedOrigins)))

	return h
}

func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if !h.grpcHandler.IsGrpcWebRequest(r) && !h.grpcHandler.IsAcceptableGrpcCorsRequest(r) {
		h.Router.ServeHTTP(w, r)
		return
	}
	h.grpcHandler.ServeHTTP(w, r)
}

func (h *httpHandler) handleClip(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(context.Background(), getPeaksForSegmentTimeout)
	defer cancel()

	if err := r.ParseForm(); err != nil {
		h.logger.With("err", err).Info("error parsing form")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	var params struct {
		StartFrame int64  `schema:"start_frame,required"`
		EndFrame   int64  `schema:"end_frame,required"`
		Format     string `schema:"format,required"`
	}
	decoder := schema.NewDecoder()
	if err := decoder.Decode(&params, r.PostForm); err != nil {
		h.logger.With("err", err).Info("error decoding form")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	vars := mux.Vars(r)
	id, err := uuid.Parse(vars["id"])
	if err != nil {
		h.logger.With("err", err).Info("error parsing ID")
		w.WriteHeader(http.StatusNotFound)
		return
	}

	var format media.AudioFormat
	switch params.Format {
	case "mp3":
		format = media.AudioFormatMP3
	case "wav":
		format = media.AudioFormatWAV
	default:
		h.logger.Info("bad format")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	stream, err := h.mediaSetService.GetAudioSegment(ctx, id, params.StartFrame, params.EndFrame, format)
	if err != nil {
		h.logger.With("err", err).Info("error getting audio segment")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Header().Set("content-type", "audio/"+format.String())
	w.Header().Set("content-disposition", "attachment; filename=clip."+format.String())
	w.WriteHeader(http.StatusOK)

	var closing bool
	for {
		progress, err := stream.Next(ctx)
		if err == io.EOF {
			closing = true
		} else if err != nil {
			h.logger.With("err", err).Error("error reading audio segment stream")
			return
		}

		w.Write(progress.Data)

		if closing {
			break
		}
	}
}

// indexedFileSystem is an HTTP file system which handles index.html files if
// they exist, but does not serve directory listings.
//
// Ref: https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings
type indexedFileSystem struct {
	httpFS http.FileSystem
}

func (ifs *indexedFileSystem) Open(path string) (http.File, error) {
	f, err := ifs.httpFS.Open(path)
	if err != nil {
		return nil, err
	}

	s, err := f.Stat()
	if err != nil {
		return nil, err
	}
	if !s.IsDir() {
		return f, nil
	}

	index := filepath.Join(path, "index.html")
	if _, err := ifs.httpFS.Open(index); err != nil {
		_ = f.Close() // ignore error
		return nil, err
	}

	return f, nil
}
