package radius

import (
	"context"
	ttls "crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"errors"
	"net/url"

	eap "beryju.io/radius-eap"
	"beryju.io/radius-eap/protocol"
	"beryju.io/radius-eap/protocol/identity"
	"beryju.io/radius-eap/protocol/legacy_nak"
	"beryju.io/radius-eap/protocol/peap"
	"beryju.io/radius-eap/protocol/tls"
	log "github.com/sirupsen/logrus"
	"goauthentik.io/internal/outpost/flow"
	"goauthentik.io/internal/utils"
	"layeh.com/radius"
	"layeh.com/radius/rfc2869"
)

func (rs *RadiusServer) Handle_AccessRequest_EAP(w radius.ResponseWriter, r *RadiusRequest) {
	er := rfc2869.EAPMessage_Get(r.Packet)
	ep, err := eap.Decode(r.pi, er)
	if err != nil {
		rs.log.WithError(err).Warning("failed to parse EAP packet")
		return
	}
	ep.HandleRadiusPacket(w, r.Request)
}

func (pi *ProviderInstance) GetEAPState(key string) *protocol.State {
	return pi.eapState[key]
}

func (pi *ProviderInstance) SetEAPState(key string, state *protocol.State) {
	pi.eapState[key] = state
}

func (pi *ProviderInstance) GetEAPSettings() protocol.Settings {
	settings := protocol.Settings{
		Logger: &logrusAdapter{pi.log},
		Protocols: []protocol.ProtocolConstructor{
			identity.Protocol,
			legacy_nak.Protocol,
		},
	}

	certId := pi.certId
	if certId == "" {
		return settings
	}

	cert := pi.s.cryptoStore.Get(certId)
	if cert == nil {
		return settings
	}

	settings.Protocols = append(settings.Protocols, tls.Protocol, peap.Protocol)
	settings.ProtocolPriority = []protocol.Type{
		identity.TypeIdentity,
		tls.TypeTLS,
	}
	settings.ProtocolSettings = map[protocol.Type]any{
		tls.TypeTLS: tls.Settings{
			Config: &ttls.Config{
				Certificates: []ttls.Certificate{*cert},
				ClientAuth:   ttls.RequireAnyClientCert,
			},
			HandshakeSuccessful: func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status {
				ident := ctx.GetProtocolState(identity.TypeIdentity).(*identity.State).Identity

				ctx.Log().Debug("Starting authn flow")
				pem := pem.EncodeToMemory(&pem.Block{
					Type:  "CERTIFICATE",
					Bytes: certs[0].Raw,
				})

				fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
					"client":   utils.GetIP(ctx.Packet().RemoteAddr),
					"identity": ident,
				})
				fe.Answers[flow.StageIdentification] = ident
				fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
				fe.Params.Add("goauthentik.io/outpost/radius", "true")
				fe.AddHeader("X-Authentik-Outpost-Certificate", url.QueryEscape(string(pem)))

				passed, err := fe.Execute()
				if err != nil {
					ctx.Log().Warn("failed to execute flow", "error", err)
					return protocol.StatusError
				}
				ctx.Log().Debug("Finished flow")
				if !passed {
					return protocol.StatusError
				}
				access, _, err := fe.ApiClient().OutpostsAPI.OutpostsRadiusAccessCheck(context.Background(), pi.providerId).AppSlug(pi.appSlug).Execute()
				if err != nil {
					ctx.Log().Warn("failed to check access: %v", err)
					return protocol.StatusError
				}
				if !access.Access.Passing {
					ctx.Log().Info("Access denied for user")
					return protocol.StatusError
				}
				if access.HasAttributes() {
					ctx.AddResponseModifier(func(r, q *radius.Packet) error {
						rawData, err := base64.StdEncoding.DecodeString(access.GetAttributes())
						if err != nil {
							ctx.Log().Warn("failed to decode attributes from core: %v", err)
							return errors.New("attribute_decode_failed")
						}
						p, err := radius.Parse(rawData, pi.SharedSecret)
						if err != nil {
							ctx.Log().Warn("failed to parse attributes from core: %v", err)
							return errors.New("attribute_parse_failed")
						}
						for _, attr := range p.Attributes {
							r.Add(attr.Type, attr.Attribute)
						}
						return nil
					})
				}
				return protocol.StatusSuccess
			},
		},
	}
	return settings
}
