// Package web Cozy Stack API.
//
// Cozy is a personal platform as a service with a focus on data.
package web

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"path"
	"time"

	"github.com/cozy/cozy-stack/model/app"
	"github.com/cozy/cozy-stack/model/stack"
	"github.com/cozy/cozy-stack/pkg/assets"
	build "github.com/cozy/cozy-stack/pkg/config"
	"github.com/cozy/cozy-stack/pkg/config/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/i18n"
	"github.com/cozy/cozy-stack/pkg/logger"
	"github.com/cozy/cozy-stack/pkg/utils"
	"github.com/cozy/cozy-stack/web/apps"
	"github.com/sirupsen/logrus"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

// ReadHeaderTimeout is the amount of time allowed to read request headers for
// all servers. This is activated for all handlers from all http servers
// created by the stack.
var ReadHeaderTimeout = 15 * time.Second

var (
	ErrMissingArgument = errors.New("the argument is missing")
)

// LoadSupportedLocales reads the po files packed in go or from the assets directory
// and loads them for translations
func LoadSupportedLocales() error {
	// By default, use the po files packed in the binary
	// but use assets from the disk is assets option is filled in config
	assetsPath := config.GetConfig().Assets
	if assetsPath != "" {
		for _, locale := range consts.SupportedLocales {
			pofile := path.Join(assetsPath, "locales", locale+".po")
			po, err := os.ReadFile(pofile)
			if err != nil {
				return fmt.Errorf("Can't load the po file for %s", locale)
			}
			i18n.LoadLocale(locale, "", po)
		}
		return nil
	}

	for _, locale := range consts.SupportedLocales {
		f, err := assets.Open("/locales/"+locale+".po", config.DefaultInstanceContext)
		if err != nil {
			return fmt.Errorf("Can't load the po file for %s", locale)
		}
		po, err := io.ReadAll(f)
		if err != nil {
			return err
		}
		i18n.LoadLocale(locale, "", po)
	}
	return nil
}

// ListenAndServeWithAppDir creates and setup all the necessary http endpoints
// and serve the specified application on a app subdomain.
//
// In order to serve the application, the specified directory should provide
// a manifest.webapp file that will be used to parameterize the application
// permissions.
func ListenAndServeWithAppDir(appsdir map[string]string, services *stack.Services) (*Servers, error) {
	for slug, dir := range appsdir {
		dir = utils.AbsPath(dir)
		appsdir[slug] = dir
		exists, err := utils.DirExists(dir)
		if err != nil {
			return nil, err
		}
		if !exists {
			logger.WithNamespace("dev").Warnf("Directory %s does not exist", dir)
		} else {
			if err = checkExists(path.Join(dir, app.WebappManifestName)); err != nil {
				logger.WithNamespace("dev").Warnf("The app manifest is missing: %s", err)
			}
			if err = checkExists(path.Join(dir, "index.html")); err != nil {
				logger.WithNamespace("dev").Warnf("The index.html is missing: %s", err)
			}
		}
	}

	app.SetupAppsDir(appsdir)
	return ListenAndServe(services)
}

func checkExists(filepath string) error {
	exists, err := utils.FileExists(filepath)
	if err != nil {
		return err
	}
	if !exists {
		return fmt.Errorf("Directory %s should contain a %s file",
			path.Dir(filepath), path.Base(filepath))
	}
	return nil
}

// ListenAndServe creates and setups all the necessary http endpoints and start
// them.
func ListenAndServe(services *stack.Services) (*Servers, error) {
	e := echo.New()
	e.HideBanner = true
	e.HidePort = true

	major, err := CreateSubdomainProxy(e, services, apps.Serve)
	if err != nil {
		return nil, err
	}
	if err = LoadSupportedLocales(); err != nil {
		return nil, err
	}

	if build.IsDevRelease() {
		timeFormat := "time_rfc3339"
		if logrus.GetLevel() == logrus.DebugLevel {
			timeFormat = "time_rfc3339_nano"
		}
		major.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
			Format: "time=${" + timeFormat + "}\tstatus=${status}\tmethod=${method}\thost=${host}\turi=${uri}\tbytes_out=${bytes_out}\n",
		}))
	}

	admin := echo.New()
	admin.HideBanner = true
	admin.HidePort = true

	if err = SetupAdminRoutes(admin); err != nil {
		return nil, err
	}

	servers := NewServers()
	err = servers.Start(major, "major", config.ServerAddr())
	if err != nil {
		return nil, fmt.Errorf("failed to start major server: %w", err)
	}

	err = servers.Start(admin, "admin", config.AdminServerAddr())
	if err != nil {
		return nil, fmt.Errorf("failed to start admin server: %w", err)
	}

	return servers, nil
}

// Servers allow to start several [echo.Echo] servers and stop them together.
//
//	It also take care of several other task:
//	- It sanitize the hosts format
//	- It exposes the handlers on several addresses if needed
//	- It forces the IPv4/IPv6 dual stack mode for `localhost` by
//	  remplacing this entry by `["127.0.0.1", "::1]`
type Servers struct {
	serversByName   map[string]*http.Server
	listenersByName map[string]net.Listener
	errs            chan error
}

func NewServers() *Servers {
	return &Servers{
		serversByName:   map[string]*http.Server{},
		listenersByName: map[string]net.Listener{},
		errs:            make(chan error),
	}
}

// Start the server 'e' to the given addrs.
//
// The 'addrs' arguments must be in the format `"host:port"`. If the host
// is not a valid IPv4/IPv6/hostname or if the port not present an error is
// returned.
func (s *Servers) Start(handler http.Handler, name string, addr string) error {
	addrs := []string{}

	if len(addr) == 0 {
		return fmt.Errorf("args: %w", ErrMissingArgument)
	}

	if len(name) == 0 {
		return fmt.Errorf("name: %w", ErrMissingArgument)
	}

	host, port, err := net.SplitHostPort(addr)
	if err != nil {
		return err
	}

	fmt.Fprintf(os.Stdout, "http server %s started on %q\n", name, addr)
	switch host {
	case "localhost":
		addrs = append(addrs, net.JoinHostPort("127.0.0.1", port))
		addrs = append(addrs, net.JoinHostPort("::1", port))
	default:
		addrs = append(addrs, net.JoinHostPort(host, port))
	}

	for _, addr := range addrs {
		l, err := net.Listen("tcp", addr)
		if err != nil {
			return err
		}

		writer := logger.WithNamespace("stack").Writer()
		logger := log.New(writer, "", 0)
		server := &http.Server{
			Addr:              addr,
			Handler:           handler,
			ReadHeaderTimeout: ReadHeaderTimeout,
			ErrorLog:          logger,
		}

		s.serversByName[name] = server
		s.listenersByName[name] = l

		go func(server *http.Server) {
			s.errs <- server.Serve(l)
		}(server)
	}

	return nil
}

// GetAddr return the address where the given server listen to.
//
// This endpoint is mostly used when we use dynamic port attribution
// like when we don't specify a port
func (s *Servers) GetAddr(name string) net.Addr {
	l, ok := s.listenersByName[name]
	if !ok {
		return nil
	}

	return l.Addr()
}

// Wait for servers to stop or fall in error.
func (s *Servers) Wait() <-chan error {
	return s.errs
}

// Shutdown gracefully stops the servers.
func (s *Servers) Shutdown(ctx context.Context) error {
	shutdowners := []utils.Shutdowner{}

	for _, server := range s.serversByName {
		shutdowners = append(shutdowners, server)
	}

	g := utils.NewGroupShutdown(shutdowners...)

	fmt.Print("  shutting down servers...")
	if err := g.Shutdown(ctx); err != nil {
		fmt.Println("failed: ", err.Error())
		return err
	}

	fmt.Println("ok.")

	return nil
}
