package server

import (
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"strings"

	"github.com/gorilla/mux"

	"github.com/Sirupsen/logrus"
	"github.com/docker/distribution/registry/api/errcode"
	"github.com/docker/docker/api"
	"github.com/docker/docker/context"
	"github.com/docker/docker/daemon"
	"github.com/docker/docker/pkg/sockets"
	"github.com/docker/docker/pkg/stringid"
	"github.com/docker/docker/utils"
)

// Config provides the configuration for the API server
type Config struct {
	Logging     bool
	EnableCors  bool
	CorsHeaders string
	Version     string
	SocketGroup string
	TLSConfig   *tls.Config
}

// Server contains instance details for the server
type Server struct {
	daemon  *daemon.Daemon
	cfg     *Config
	router  *mux.Router
	start   chan struct{}
	servers []serverCloser
}

// New returns a new instance of the server based on the specified configuration.
func New(ctx context.Context, cfg *Config) *Server {
	srv := &Server{
		cfg:   cfg,
		start: make(chan struct{}),
	}
	srv.router = createRouter(ctx, srv)
	return srv
}

// Close closes servers and thus stop receiving requests
func (s *Server) Close() {
	for _, srv := range s.servers {
		if err := srv.Close(); err != nil {
			logrus.Error(err)
		}
	}
}

type serverCloser interface {
	Serve() error
	Close() error
}

// ServeAPI loops through all of the protocols sent in to docker and spawns
// off a go routine to setup a serving http.Server for each.
func (s *Server) ServeAPI(protoAddrs []string) error {
	var chErrors = make(chan error, len(protoAddrs))

	for _, protoAddr := range protoAddrs {
		protoAddrParts := strings.SplitN(protoAddr, "://", 2)
		if len(protoAddrParts) != 2 {
			return fmt.Errorf("bad format, expected PROTO://ADDR")
		}
		srv, err := s.newServer(protoAddrParts[0], protoAddrParts[1])
		if err != nil {
			return err
		}
		s.servers = append(s.servers, srv...)

		for _, s := range srv {
			logrus.Infof("Listening for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1])
			go func(s serverCloser) {
				if err := s.Serve(); err != nil && strings.Contains(err.Error(), "use of closed network connection") {
					err = nil
				}
				chErrors <- err
			}(s)
		}
	}

	for i := 0; i < len(protoAddrs); i++ {
		err := <-chErrors
		if err != nil {
			return err
		}
	}

	return nil
}

// HTTPServer contains an instance of http server and the listener.
// srv *http.Server, contains configuration to create a http server and a mux router with all api end points.
// l   net.Listener, is a TCP or Socket listener that dispatches incoming request to the router.
type HTTPServer struct {
	srv *http.Server
	l   net.Listener
}

// Serve starts listening for inbound requests.
func (s *HTTPServer) Serve() error {
	return s.srv.Serve(s.l)
}

// Close closes the HTTPServer from listening for the inbound requests.
func (s *HTTPServer) Close() error {
	return s.l.Close()
}

// HTTPAPIFunc is an adapter to allow the use of ordinary functions as Docker API endpoints.
// Any function that has the appropriate signature can be register as a API endpoint (e.g. getVersion).
type HTTPAPIFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error

func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) {
	conn, _, err := w.(http.Hijacker).Hijack()
	if err != nil {
		return nil, nil, err
	}
	// Flush the options to make sure the client sets the raw mode
	conn.Write([]byte{})
	return conn, conn, nil
}

func closeStreams(streams ...interface{}) {
	for _, stream := range streams {
		if tcpc, ok := stream.(interface {
			CloseWrite() error
		}); ok {
			tcpc.CloseWrite()
		} else if closer, ok := stream.(io.Closer); ok {
			closer.Close()
		}
	}
}

// checkForJSON makes sure that the request's Content-Type is application/json.
func checkForJSON(r *http.Request) error {
	ct := r.Header.Get("Content-Type")

	// No Content-Type header is ok as long as there's no Body
	if ct == "" {
		if r.Body == nil || r.ContentLength == 0 {
			return nil
		}
	}

	// Otherwise it better be json
	if api.MatchesContentType(ct, "application/json") {
		return nil
	}
	return fmt.Errorf("Content-Type specified (%s) must be 'application/json'", ct)
}

//If we don't do this, POST method without Content-type (even with empty body) will fail
func parseForm(r *http.Request) error {
	if r == nil {
		return nil
	}
	if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
		return err
	}
	return nil
}

func parseMultipartForm(r *http.Request) error {
	if err := r.ParseMultipartForm(4096); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
		return err
	}
	return nil
}

func httpError(w http.ResponseWriter, err error) {
	if err == nil || w == nil {
		logrus.WithFields(logrus.Fields{"error": err, "writer": w}).Error("unexpected HTTP error handling")
		return
	}

	statusCode := http.StatusInternalServerError
	errMsg := err.Error()

	// Based on the type of error we get we need to process things
	// slightly differently to extract the error message.
	// In the 'errcode.*' cases there are two different type of
	// error that could be returned. errocode.ErrorCode is the base
	// type of error object - it is just an 'int' that can then be
	// used as the look-up key to find the message. errorcode.Error
	// extends errorcode.Error by adding error-instance specific
	// data, like 'details' or variable strings to be inserted into
	// the message.
	//
	// Ideally, we should just be able to call err.Error() for all
	// cases but the errcode package doesn't support that yet.
	//
	// Additionally, in both errcode cases, there might be an http
	// status code associated with it, and if so use it.
	switch err.(type) {
	case errcode.ErrorCode:
		daError, _ := err.(errcode.ErrorCode)
		statusCode = daError.Descriptor().HTTPStatusCode
		errMsg = daError.Message()

	case errcode.Error:
		// For reference, if you're looking for a particular error
		// then you can do something like :
		//   import ( derr "github.com/docker/docker/errors" )
		//   if daError.ErrorCode() == derr.ErrorCodeNoSuchContainer { ... }

		daError, _ := err.(errcode.Error)
		statusCode = daError.ErrorCode().Descriptor().HTTPStatusCode
		errMsg = daError.Message

	default:
		// This part of will be removed once we've
		// converted everything over to use the errcode package

		// FIXME: this is brittle and should not be necessary.
		// If we need to differentiate between different possible error types,
		// we should create appropriate error types with clearly defined meaning
		errStr := strings.ToLower(err.Error())
		for keyword, status := range map[string]int{
			"not found":             http.StatusNotFound,
			"no such":               http.StatusNotFound,
			"bad parameter":         http.StatusBadRequest,
			"conflict":              http.StatusConflict,
			"impossible":            http.StatusNotAcceptable,
			"wrong login/password":  http.StatusUnauthorized,
			"hasn't been activated": http.StatusForbidden,
		} {
			if strings.Contains(errStr, keyword) {
				statusCode = status
				break
			}
		}
	}

	if statusCode == 0 {
		statusCode = http.StatusInternalServerError
	}

	logrus.WithFields(logrus.Fields{"statusCode": statusCode, "err": utils.GetErrorMessage(err)}).Error("HTTP Error")
	http.Error(w, errMsg, statusCode)
}

// writeJSON writes the value v to the http response stream as json with standard
// json encoding.
func writeJSON(w http.ResponseWriter, code int, v interface{}) error {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	return json.NewEncoder(w).Encode(v)
}

func (s *Server) optionsHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	w.WriteHeader(http.StatusOK)
	return nil
}
func writeCorsHeaders(w http.ResponseWriter, r *http.Request, corsHeaders string) {
	logrus.Debugf("CORS header is enabled and set to: %s", corsHeaders)
	w.Header().Add("Access-Control-Allow-Origin", corsHeaders)
	w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, X-Registry-Auth")
	w.Header().Add("Access-Control-Allow-Methods", "HEAD, GET, POST, DELETE, PUT, OPTIONS")
}

func (s *Server) ping(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	_, err := w.Write([]byte{'O', 'K'})
	return err
}

func (s *Server) initTCPSocket(addr string) (l net.Listener, err error) {
	if s.cfg.TLSConfig == nil || s.cfg.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert {
		logrus.Warn("/!\\ DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
	}
	if l, err = sockets.NewTCPSocket(addr, s.cfg.TLSConfig, s.start); err != nil {
		return nil, err
	}
	if err := allocateDaemonPort(addr); err != nil {
		return nil, err
	}
	return
}

func (s *Server) makeHTTPHandler(ctx context.Context, localMethod string, localRoute string, localHandler HTTPAPIFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// log the handler generation
		logrus.Debugf("Calling %s %s", localMethod, localRoute)

		// Define the context that we'll pass around to share info
		// like the docker-request-id.
		//
		// The 'context' will be used for global data that should
		// apply to all requests. Data that is specific to the
		// immediate function being called should still be passed
		// as 'args' on the function call.
		reqID := stringid.TruncateID(stringid.GenerateNonCryptoID())
		ctx = context.WithValue(ctx, context.RequestID, reqID)
		handlerFunc := s.handleWithGlobalMiddlewares(localHandler)

		if err := handlerFunc(ctx, w, r, mux.Vars(r)); err != nil {
			logrus.Errorf("Handler for %s %s returned error: %s", localMethod, localRoute, utils.GetErrorMessage(err))
			httpError(w, err)
		}
	}
}

// createRouter initializes the main router the server uses.
// we keep enableCors just for legacy usage, need to be removed in the future
func createRouter(ctx context.Context, s *Server) *mux.Router {
	r := mux.NewRouter()
	if os.Getenv("DEBUG") != "" {
		profilerSetup(r, "/debug/")
	}
	m := map[string]map[string]HTTPAPIFunc{
		"HEAD": {
			"/containers/{name:.*}/archive": s.headContainersArchive,
		},
		"GET": {
			"/_ping":                          s.ping,
			"/events":                         s.getEvents,
			"/info":                           s.getInfo,
			"/version":                        s.getVersion,
			"/images/json":                    s.getImagesJSON,
			"/images/search":                  s.getImagesSearch,
			"/images/get":                     s.getImagesGet,
			"/images/{name:.*}/get":           s.getImagesGet,
			"/images/{name:.*}/history":       s.getImagesHistory,
			"/images/{name:.*}/json":          s.getImagesByName,
			"/containers/json":                s.getContainersJSON,
			"/containers/{name:.*}/export":    s.getContainersExport,
			"/containers/{name:.*}/changes":   s.getContainersChanges,
			"/containers/{name:.*}/json":      s.getContainersByName,
			"/containers/{name:.*}/top":       s.getContainersTop,
			"/containers/{name:.*}/logs":      s.getContainersLogs,
			"/containers/{name:.*}/stats":     s.getContainersStats,
			"/containers/{name:.*}/attach/ws": s.wsContainersAttach,
			"/exec/{id:.*}/json":              s.getExecByID,
			"/containers/{name:.*}/archive":   s.getContainersArchive,
			"/volumes":                        s.getVolumesList,
			"/volumes/{name:.*}":              s.getVolumeByName,
		},
		"POST": {
			"/auth":                         s.postAuth,
			"/commit":                       s.postCommit,
			"/build":                        s.postBuild,
			"/images/create":                s.postImagesCreate,
			"/images/load":                  s.postImagesLoad,
			"/images/{name:.*}/push":        s.postImagesPush,
			"/images/{name:.*}/tag":         s.postImagesTag,
			"/containers/create":            s.postContainersCreate,
			"/containers/{name:.*}/kill":    s.postContainersKill,
			"/containers/{name:.*}/pause":   s.postContainersPause,
			"/containers/{name:.*}/unpause": s.postContainersUnpause,
			"/containers/{name:.*}/restart": s.postContainersRestart,
			"/containers/{name:.*}/start":   s.postContainersStart,
			"/containers/{name:.*}/stop":    s.postContainersStop,
			"/containers/{name:.*}/wait":    s.postContainersWait,
			"/containers/{name:.*}/resize":  s.postContainersResize,
			"/containers/{name:.*}/attach":  s.postContainersAttach,
			"/containers/{name:.*}/copy":    s.postContainersCopy,
			"/containers/{name:.*}/exec":    s.postContainerExecCreate,
			"/exec/{name:.*}/start":         s.postContainerExecStart,
			"/exec/{name:.*}/resize":        s.postContainerExecResize,
			"/containers/{name:.*}/rename":  s.postContainerRename,
			"/volumes":                      s.postVolumesCreate,
		},
		"PUT": {
			"/containers/{name:.*}/archive": s.putContainersArchive,
		},
		"DELETE": {
			"/containers/{name:.*}": s.deleteContainers,
			"/images/{name:.*}":     s.deleteImages,
			"/volumes/{name:.*}":    s.deleteVolumes,
		},
		"OPTIONS": {
			"": s.optionsHandler,
		},
	}

	for method, routes := range m {
		for route, fct := range routes {
			logrus.Debugf("Registering %s, %s", method, route)
			// NOTE: scope issue, make sure the variables are local and won't be changed
			localRoute := route
			localFct := fct
			localMethod := method

			// build the handler function
			f := s.makeHTTPHandler(ctx, localMethod, localRoute, localFct)

			// add the new route
			if localRoute == "" {
				r.Methods(localMethod).HandlerFunc(f)
			} else {
				r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f)
				r.Path(localRoute).Methods(localMethod).HandlerFunc(f)
			}
		}
	}

	return r
}