package api

import (
	"fmt"
	"net/http"
	"os"
	"regexp"
	"time"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"gitlab.com/uafrica/go-utils/errors"
	"gitlab.com/uafrica/go-utils/logger"
	"gitlab.com/uafrica/go-utils/service"
)

//New creates the API with the specified routes keys on [path][method]
//value could be any of the handler function signatures supported by the api.Router
//requestIDHeaderKey is defined in the response header to match the requestID from the request
func New(requestIDHeaderKey string, routes map[string]map[string]interface{}) Api {
	env := os.Getenv("ENVIRONMENT") //todo: support config loading for local dev and env for lambda in prod
	if env == "" {
		env = "dev"
	}
	if requestIDHeaderKey == "" {
		requestIDHeaderKey = "request-id"
	}

	router, err := NewRouter(routes)
	if err != nil {
		panic(fmt.Sprintf("cannot create router: %+v", err))
	}

	return Api{
		ILogger:            logger.New().WithFields(map[string]interface{}{"env": env}),
		env:                env,
		router:             router,
		requestIDHeaderKey: requestIDHeaderKey,
		checks:             []check{},
		crashReporter:      defaultCrashReporter{},
		auditor:            noAudit{},
	}
}

type Api struct {
	logger.ILogger     //for logging outside of context
	env                string
	router             Router
	requestIDHeaderKey string

	//options:
	localPort     int //==0 for lambda, >0 for http.ListenAndServe
	crashReporter ICrashReporter
	cors          ICORS
	dbConn        service.IDatabaseConnector
	checks        []check
	auditor       IAuditor
}

//checks are executed at the start of each request
//to stop processing, return an error, preferably with an HTTP status code (see errors.HTTP(...))
//all values returned will be logged and added to the context for retrieval by any handler
//the values are added with the check's name as prefix: "<check.name>_<value.name>"
//and cannot be changed afterwards
type IChecker interface {
	Check(req events.APIGatewayProxyRequest) (values map[string]interface{}, err error)
}

type check struct {
	name    string
	checker IChecker
}

const namePattern = `[a-z]([a-z0-9_]*[a-z0-9])*`

var nameRegex = regexp.MustCompile("^" + namePattern + "$")

//check name must be lowercase with optional underscores, e.g. "rate_limiter" or "claims"
func (app Api) WithCheck(name string, checker IChecker) Api {
	if !nameRegex.MatchString(name) {
		panic(errors.Errorf("invalid name for check(%s)", name))
	}
	if checker == nil {
		panic(errors.Errorf("check(%s) is nil", name))
	}
	for _, check := range app.checks {
		if check.name == name {
			panic(errors.Errorf("check(%s) already registered", name))
		}
	}
	app.checks = append(app.checks, check{name: name, checker: checker})
	return app
}

func (api Api) WithDb(dbConn service.IDatabaseConnector) Api {
	api.dbConn = dbConn
	return api
}

func (api Api) WithCORS(cors ICORS) Api {
	api.cors = cors
	return api
}

type ICORS interface {
	CORS() map[string]string //return CORS headers
}

func (api Api) WithCrashReported(crashReporter ICrashReporter) Api {
	if crashReporter != nil {
		api.crashReporter = crashReporter
	}
	return api
}

type ICrashReporter interface {
	Catch(ctx Context) //Report(method string, path string, crash interface{})
}

func (api Api) WithLocalPort(localPortPtr *int) Api {
	if localPortPtr != nil && *localPortPtr > 0 {
		api.localPort = *localPortPtr
	}
	return api
}

//run and panic on error
func (api Api) Run() {
	//decide local of lambda
	if api.localPort > 0 {
		err := http.ListenAndServe(fmt.Sprintf(":%d", api.localPort), api) //calls app.ServeHTTP() which calls app.Handler()
		if err != nil {
			panic(err)
		}
	} else {
		lambda.Start(api.Handler) //calls app.Handler directly
	}
}

type defaultCrashReporter struct{}

func (defaultCrashReporter) Catch(ctx Context) {
	// crash := recover()
	// if crash != nil {
	// 	ctx.Errorf("CRASH: (%T) %+v\n", crash, crash)
	// }
}

type IAuditor interface {
	Audit(startTime, endTime time.Time, values map[string]interface{})
}

type noAudit struct{}

func (noAudit) Audit(startTime, endTime time.Time, values map[string]interface{}) {} //do nothing