package server

import (
	"context"
	"encoding/json"
	"fmt"
	"mime"
	"net"
	"net/http"
	"os"
	"path"
	"slices"
	"strings"
	"time"

	cfg "website/internal/config"
	"website/internal/log"

	"github.com/benpate/digit"
	"github.com/getsentry/sentry-go"
	sentryhttp "github.com/getsentry/sentry-go/http"
	"github.com/pkg/errors"
)

var config *cfg.Config

var (
	CommitSHA string
	ShortSHA  string
)

type Config struct {
	Production    bool    `conf:"default:false"`
	InDevServer   bool    `conf:"default:false"`
	Root          string  `conf:"default:website"`
	ListenAddress string  `conf:"default:localhost"`
	Port          string  `conf:"default:3000,short:p"`
	BaseURL       cfg.URL `conf:"default:http://localhost:3000,short:b"`
}

type HTTPError struct {
	Error   error
	Message string
	Code    int
}

type Server struct {
	*http.Server
}

func canonicalisePath(path string) (cPath string, differs bool) {
	cPath = path
	if strings.HasSuffix(path, "/index.html") {
		cPath, differs = strings.CutSuffix(path, "index.html")
	} else if !strings.HasSuffix(path, "/") && files[path+"/"] != (File{}) {
		cPath, differs = path+"/", true
	}
	return cPath, differs
}

func serveFile(w http.ResponseWriter, r *http.Request) *HTTPError {
	urlPath, shouldRedirect := canonicalisePath(r.URL.Path)
	if shouldRedirect {
		http.Redirect(w, r, urlPath, 302)
		return nil
	}
	file := GetFile(urlPath)
	if file == (File{}) {
		return &HTTPError{
			Message: "File not found",
			Code:    http.StatusNotFound,
		}
	}
	w.Header().Add("ETag", file.etag)
	w.Header().Add("Vary", "Accept-Encoding")
	w.Header().Add("Content-Security-Policy", config.CSP.String())
	for k, v := range config.Extra.Headers {
		w.Header().Add(k, v)
	}

	http.ServeFile(w, r, files[urlPath].filename)
	return nil
}

type webHandler func(http.ResponseWriter, *http.Request) *HTTPError

func (fn webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	defer func() {
		if fail := recover(); fail != nil {
			w.WriteHeader(http.StatusInternalServerError)
			log.Error("runtime panic!", "error", fail)
		}
	}()
	w.Header().Set("Server", fmt.Sprintf("website (%s)", ShortSHA))
	if err := fn(w, r); err != nil {
		if strings.Contains(r.Header.Get("Accept"), "text/html") {
			w.WriteHeader(err.Code)
			notFoundPage := "website/private/404.html"
			http.ServeFile(w, r, notFoundPage)
		} else {
			http.Error(w, err.Message, err.Code)
		}
	}
}

var newMIMEs = map[string]string{
	".xsl": "text/xsl",
}

func fixupMIMETypes() {
	for ext, newType := range newMIMEs {
		if err := mime.AddExtensionType(ext, newType); err != nil {
			log.Error("could not update mime type", "ext", ext, "mime", newType)
		}
	}
}

func applyDevModeOverrides(config *cfg.Config) {
	config.CSP.ScriptSrc = slices.Insert(config.CSP.ScriptSrc, 0, "'unsafe-inline'")
	config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'")
}

func New(runtimeConfig *Config) (*Server, error) {
	fixupMIMETypes()

	var err error
	config, err = cfg.GetConfig()
	if err != nil {
		return nil, errors.WithMessage(err, "error parsing configuration file")
	}
	if runtimeConfig.InDevServer {
		applyDevModeOverrides(config)
	}

	prefix := path.Join(runtimeConfig.Root, "public")
	log.Debug("registering content files", "prefix", prefix)
	err = registerContentFiles(prefix)
	if err != nil {
		return nil, errors.WithMessagef(err, "registering content files")
	}

	env := "development"
	if runtimeConfig.Production {
		env = "production"
	}
	err = sentry.Init(sentry.ClientOptions{
		EnableTracing:    true,
		TracesSampleRate: 1.0,
		Dsn:              os.Getenv("SENTRY_DSN"),
		Release:          CommitSHA,
		Environment:      env,
	})
	if err != nil {
		return nil, errors.WithMessage(err, "could not set up sentry")
	}
	defer sentry.Flush(2 * time.Second)
	sentryHandler := sentryhttp.New(sentryhttp.Options{
		Repanic: true,
	})

	top := http.NewServeMux()
	mux := http.NewServeMux()
	log.Debug("binding main handler to", "host", runtimeConfig.BaseURL.Hostname()+"/")
	hostname := runtimeConfig.BaseURL.Hostname()
	mux.Handle(hostname+"/", webHandler(serveFile))

	var acctResource = "acct:" + config.Email
	me := digit.NewResource(acctResource).
		Link("http://openid.net/specs/connect/1.0/issuer", "", config.OIDCHost.String())
	mux.HandleFunc(hostname+"/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("resource") == acctResource {
			obj, err := json.Marshal(me)
			if err != nil {
				http.Error(
					w,
					http.StatusText(http.StatusInternalServerError),
					http.StatusInternalServerError,
				)

				return
			}

			w.Header().Add("Content-Type", "application/jrd+json")
			w.Header().Add("Access-Control-Allow-Origin", "*")
			_, err = w.Write(obj)
			if err != nil {
				log.Warn("error writing webfinger request", "error", err)
			}
		}
	})
	const oidcPath = "/.well-known/openid-configuration"
	mux.HandleFunc(
		hostname+oidcPath,
		func(w http.ResponseWriter, r *http.Request) {
			u := config.OIDCHost.JoinPath(oidcPath)
			http.Redirect(w, r, u.String(), 302)
		})

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		newURL := runtimeConfig.BaseURL.String() + r.URL.String()
		http.Redirect(w, r, newURL, 301)
	})

	top.Handle("/",
		sentryHandler.Handle(
			wrapHandlerWithLogging(mux, wrappedHandlerOptions{
				defaultHostname: runtimeConfig.BaseURL.Hostname(),
			}),
		),
	)
	// no logging, no sentry
	top.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	listenAddress := net.JoinHostPort(runtimeConfig.ListenAddress, runtimeConfig.Port)
	return &Server{
		&http.Server{
			Addr:    listenAddress,
			Handler: top,
		},
	}, nil
}

func (s *Server) Start() error {
	if err := s.ListenAndServe(); err != http.ErrServerClosed {
		return err
	}
	return nil
}

func (s *Server) Stop() chan struct{} {
	log.Debug("stop called")

	idleConnsClosed := make(chan struct{})

	go func() {
		log.Debug("shutting down server")
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		err := s.Server.Shutdown(ctx)
		log.Debug("server shut down")
		if err != nil {
			// Error from closing listeners, or context timeout:
			log.Warn("HTTP server Shutdown", "error", err)
		}
		close(idleConnsClosed)
	}()

	return idleConnsClosed
}