package rewriter

import (
	"context"
	"net/http"
	"net/url"

	"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
	"forge.cadoles.com/cadoles/bouncer/internal/rule"
	ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http"
	"forge.cadoles.com/cadoles/bouncer/internal/store"
	"github.com/pkg/errors"
)

type RequestVars struct {
	Request     RequestVar `expr:"request"`
	OriginalURL URLVar     `expr:"original_url"`
}

type URLVar struct {
	Scheme      string  `expr:"scheme"`
	Opaque      string  `expr:"opaque"`
	User        UserVar `expr:"user"`
	Host        string  `expr:"host"`
	Path        string  `expr:"path"`
	RawPath     string  `expr:"raw_path"`
	RawQuery    string  `expr:"raw_query"`
	Fragment    string  `expr:"fragment"`
	RawFragment string  `expr:"raw_fragment"`
}

func fromURL(url *url.URL) URLVar {
	return URLVar{
		Scheme: url.Scheme,
		Opaque: url.Opaque,
		User: UserVar{
			Username: url.User.Username(),
			Password: func() string {
				passwd, _ := url.User.Password()
				return passwd
			}(),
		},
		Host:        url.Host,
		Path:        url.Path,
		RawPath:     url.RawPath,
		RawQuery:    url.RawQuery,
		Fragment:    url.Fragment,
		RawFragment: url.RawFragment,
	}
}

type UserVar struct {
	Username string `expr:"username"`
	Password string `expr:"password"`
}

type RequestVar struct {
	Method           string              `expr:"method"`
	URL              URLVar              `expr:"url"`
	RawURL           string              `expr:"raw_url"`
	Proto            string              `expr:"proto"`
	ProtoMajor       int                 `expr:"proto_major"`
	ProtoMinor       int                 `expr:"proto_minor"`
	Header           map[string][]string `expr:"header"`
	ContentLength    int64               `expr:"content_length"`
	TransferEncoding []string            `expr:"transfer_encoding"`
	Host             string              `expr:"host"`
	Trailer          map[string][]string `expr:"trailer"`
	RemoteAddr       string              `expr:"remote_addr"`
	RequestURI       string              `expr:"request_uri"`
}

func fromRequest(r *http.Request) RequestVar {
	return RequestVar{
		Method:           r.Method,
		URL:              fromURL(r.URL),
		RawURL:           r.URL.String(),
		Proto:            r.Proto,
		ProtoMajor:       r.ProtoMajor,
		ProtoMinor:       r.ProtoMinor,
		Header:           r.Header,
		ContentLength:    r.ContentLength,
		TransferEncoding: r.TransferEncoding,
		Host:             r.Host,
		Trailer:          r.Trailer,
		RemoteAddr:       r.RemoteAddr,
		RequestURI:       r.RequestURI,
	}
}

func (l *Layer) applyRequestRules(ctx context.Context, r *http.Request, layer *store.Layer, options *LayerOptions) error {
	rules := options.Rules.Request
	if len(rules) == 0 {
		return nil
	}

	engine, err := l.getRequestRuleEngine(ctx, layer, options)
	if err != nil {
		return errors.WithStack(err)
	}

	originalURL, err := director.OriginalURL(ctx)
	if err != nil {
		return errors.WithStack(err)
	}

	vars := &RequestVars{
		OriginalURL: fromURL(originalURL),
		Request:     fromRequest(r),
	}

	ctx = ruleHTTP.WithRequest(ctx, r)

	if _, err := engine.Apply(ctx, vars); err != nil {
		return errors.WithStack(err)
	}

	return nil
}

func (l *Layer) getRequestRuleEngine(ctx context.Context, layer *store.Layer, options *LayerOptions) (*rule.Engine[*RequestVars], error) {
	key := string(layer.Proxy) + "-" + string(layer.Name)
	revisionedEngine := l.requestRuleEngineCache.Get(key)

	engine, err := revisionedEngine.Get(ctx, layer.Revision, options)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	return engine, nil
}

type ResponseVars struct {
	OriginalURL URLVar      `expr:"original_url"`
	Request     RequestVar  `expr:"request"`
	Response    ResponseVar `expr:"response"`
}

type ResponseVar struct {
	Status           string              `expr:"status"`
	StatusCode       int                 `expr:"status_code"`
	Proto            string              `expr:"proto"`
	ProtoMajor       int                 `expr:"proto_major"`
	ProtoMinor       int                 `expr:"proto_minor"`
	Header           map[string][]string `expr:"header"`
	ContentLength    int64               `expr:"content_length"`
	TransferEncoding []string            `expr:"transfer_encoding"`
	Uncompressed     bool                `expr:"uncompressed"`
	Trailer          map[string][]string `expr:"trailer"`
}

func (l *Layer) applyResponseRules(ctx context.Context, r *http.Response, layer *store.Layer, options *LayerOptions) error {
	rules := options.Rules.Response
	if len(rules) == 0 {
		return nil
	}

	engine, err := l.getResponseRuleEngine(ctx, layer, options)
	if err != nil {
		return errors.WithStack(err)
	}

	originalURL, err := director.OriginalURL(ctx)
	if err != nil {
		return errors.WithStack(err)
	}

	vars := &ResponseVars{
		OriginalURL: fromURL(originalURL),
		Request:     fromRequest(r.Request),
		Response: ResponseVar{
			Proto:            r.Proto,
			ProtoMajor:       r.ProtoMajor,
			ProtoMinor:       r.ProtoMinor,
			Header:           r.Header,
			ContentLength:    r.ContentLength,
			TransferEncoding: r.TransferEncoding,
			Trailer:          r.Trailer,
			Status:           r.Status,
			StatusCode:       r.StatusCode,
		},
	}

	ctx = ruleHTTP.WithResponse(ctx, r)
	ctx = ruleHTTP.WithRequest(ctx, r.Request)

	if _, err := engine.Apply(ctx, vars); err != nil {
		return errors.WithStack(err)
	}

	return nil
}

func (l *Layer) getResponseRuleEngine(ctx context.Context, layer *store.Layer, options *LayerOptions) (*rule.Engine[*ResponseVars], error) {
	key := string(layer.Proxy) + "-" + string(layer.Name)
	revisionedEngine := l.responseRuleEngineCache.Get(key)

	engine, err := revisionedEngine.Get(ctx, layer.Revision, options)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	return engine, nil
}