// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package vcweb

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path"
	"strings"
)

// authHandler serves requests only if the Basic Auth data sent with the request
// matches the contents of a ".access" file in the requested directory.
//
// For each request, the handler looks for a file named ".access" and parses it
// as a JSON-serialized accessToken. If the credentials from the request match
// the accessToken, the file is served normally; otherwise, it is rejected with
// the StatusCode and Message provided by the token.
type authHandler struct{}

type accessToken struct {
	Username, Password string
	StatusCode         int // defaults to 401.
	Message            string
}

func (h *authHandler) Available() bool { return true }

func (h *authHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) {
	fs := http.Dir(dir)

	handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		urlPath := req.URL.Path
		if urlPath != "" && strings.HasPrefix(path.Base(urlPath), ".") {
			http.Error(w, "filename contains leading dot", http.StatusBadRequest)
			return
		}

		f, err := fs.Open(urlPath)
		if err != nil {
			if os.IsNotExist(err) {
				http.NotFound(w, req)
			} else {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
			return
		}

		accessDir := urlPath
		if fi, err := f.Stat(); err == nil && !fi.IsDir() {
			accessDir = path.Dir(urlPath)
		}
		f.Close()

		var accessFile http.File
		for {
			var err error
			accessFile, err = fs.Open(path.Join(accessDir, ".access"))
			if err == nil {
				defer accessFile.Close()
				break
			}

			if !os.IsNotExist(err) {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			if accessDir == "." {
				http.Error(w, "failed to locate access file", http.StatusInternalServerError)
				return
			}
			accessDir = path.Dir(accessDir)
		}

		data, err := io.ReadAll(accessFile)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		var token accessToken
		if err := json.Unmarshal(data, &token); err != nil {
			logger.Print(err)
			http.Error(w, "malformed access file", http.StatusInternalServerError)
			return
		}
		if username, password, ok := req.BasicAuth(); !ok || username != token.Username || password != token.Password {
			code := token.StatusCode
			if code == 0 {
				code = http.StatusUnauthorized
			}
			if code == http.StatusUnauthorized {
				w.Header().Add("WWW-Authenticate", fmt.Sprintf("basic realm=%s", accessDir))
			}
			http.Error(w, token.Message, code)
			return
		}

		http.FileServer(fs).ServeHTTP(w, req)
	})

	return handler, nil
}
