package service

import (
	"crypto/hmac"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/core/app/auth"
	"github.com/1Panel-dev/1Panel/core/app/dto"
	"github.com/1Panel-dev/1Panel/core/app/repo"
	"github.com/1Panel-dev/1Panel/core/buserr"
	"github.com/1Panel-dev/1Panel/core/constant"
	"github.com/1Panel-dev/1Panel/core/global"
	"github.com/1Panel-dev/1Panel/core/init/session/psession"
	"github.com/1Panel-dev/1Panel/core/utils/encrypt"
	"github.com/1Panel-dev/1Panel/core/utils/passkey"
	"github.com/gin-gonic/gin"
	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
	"gorm.io/gorm"
)

type AuthService struct{}

type IAuthService interface {
	GetResponsePage() (string, error)
	VerifyCode(code string) (bool, error)
	LogOut(c *gin.Context) error
	PasskeyBeginLogin(c *gin.Context, entrance string) (*dto.PasskeyBeginResponse, string, error)
	PasskeyFinishLogin(c *gin.Context, sessionID, entrance string) (*dto.UserLoginInfo, string, error)
	PasskeyBeginRegister(c *gin.Context, name string) (*dto.PasskeyBeginResponse, string, error)
	PasskeyFinishRegister(c *gin.Context, sessionID string) (string, error)
	PasskeyList() ([]dto.PasskeyInfo, error)
	PasskeyDelete(id string) error
	PasskeyStatus(c *gin.Context) bool
	GetSecurityEntrance() string
	IsLogin(c *gin.Context) bool
}

func NewIAuthService() IAuthService {
	return &AuthService{}
}

func (u *AuthService) LogOut(c *gin.Context) error {
	httpsSetting, err := settingRepo.Get(repo.WithByKey("SSL"))
	if err != nil {
		return err
	}
	sID, _ := c.Cookie(constant.SessionName)
	if sID != "" {
		c.SetCookie(constant.SessionName, sID, -1, "", "", httpsSetting.Value == constant.StatusEnable, true)
		c.SetCookie(constant.CSRFTokenName, "", -1, "/", "", httpsSetting.Value == constant.StatusEnable, false)
		err := global.SESSION.Delete(c)
		if err != nil {
			return err
		}
	}
	return nil
}

func (u *AuthService) VerifyCode(code string) (bool, error) {
	setting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
	if err != nil {
		return false, err
	}
	return hmac.Equal([]byte(setting.Value), []byte(code)), nil
}

func (u *AuthService) GetResponsePage() (string, error) {
	pageCode, err := settingRepo.Get(repo.WithByKey("NoAuthSetting"))
	if err != nil {
		return "", err
	}
	return pageCode.Value, nil
}

func (u *AuthService) GetSecurityEntrance() string {
	status, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
	if err != nil {
		return ""
	}
	if len(status.Value) == 0 {
		return ""
	}
	return status.Value
}

func (u *AuthService) IsLogin(c *gin.Context) bool {
	_, err := global.SESSION.Get(c)
	return err == nil
}

func (u *AuthService) PasskeyStatus(c *gin.Context) bool {
	enabled, err := u.passkeyEnabled(c)
	if err != nil {
		global.LOG.Errorf("passkey enabled check failed, err: %v", err)
		enabled = false
	}
	configured, err := u.passkeyConfigured()
	if err != nil {
		global.LOG.Errorf("passkey config check failed, err: %v", err)
		configured = false
	}
	return enabled && configured
}

func (u *AuthService) PasskeyBeginLogin(c *gin.Context, entrance string) (*dto.PasskeyBeginResponse, string, error) {
	if err := u.checkEntrance(entrance); err != nil {
		return nil, "ErrEntrance", err
	}

	config, msgKey, err := u.passkeyConfig(c)
	if err != nil {
		return nil, msgKey, err
	}

	records, err := loadPasskeyCredentialRecords()
	if err != nil {
		return nil, "", err
	}
	if len(records) == 0 {
		return nil, "ErrPasskeyNotConfigured", buserr.New("ErrPasskeyNotConfigured")
	}

	user, err := u.passkeyUser(records, true)
	if err != nil {
		return nil, "", err
	}

	wa, err := webauthn.New(config)
	if err != nil {
		return nil, "", err
	}
	assertion, sessionData, err := wa.BeginLogin(user)
	if err != nil {
		return nil, "", err
	}
	passkeySessions := passkey.GetPasskeySessionStore()
	sessionID := passkeySessions.Set(passkey.PasskeySessionKindLogin, "", *sessionData)
	return &dto.PasskeyBeginResponse{SessionID: sessionID, PublicKey: assertion.Response}, "", nil
}

func (u *AuthService) PasskeyFinishLogin(c *gin.Context, sessionID, entrance string) (*dto.UserLoginInfo, string, error) {
	if sessionID == "" {
		return nil, "ErrPasskeySession", buserr.New("ErrPasskeySession")
	}

	if err := u.checkEntrance(entrance); err != nil {
		return nil, "ErrEntrance", err
	}

	config, msgKey, err := u.passkeyConfig(c)
	if err != nil {
		return nil, msgKey, err
	}

	passkeySessions := passkey.GetPasskeySessionStore()
	session, ok := passkeySessions.Get(sessionID)
	if !ok || session.Kind != passkey.PasskeySessionKindLogin {
		return nil, "ErrPasskeySession", buserr.New("ErrPasskeySession")
	}
	passkeySessions.Delete(sessionID)

	records, err := loadPasskeyCredentialRecords()
	if err != nil {
		return nil, "", err
	}
	if len(records) == 0 {
		return nil, "ErrPasskeyNotConfigured", buserr.New("ErrPasskeyNotConfigured")
	}

	user, err := u.passkeyUser(records, true)
	if err != nil {
		return nil, "", err
	}

	wa, err := webauthn.New(config)
	if err != nil {
		return nil, "", err
	}
	credential, err := wa.FinishLogin(user, session.Session, c.Request)
	if err != nil {
		return nil, "ErrAuth", err
	}

	if err := updatePasskeyCredentialRecord(records, credential); err != nil {
		return nil, "ErrAuth", err
	}
	if err := savePasskeyCredentialRecords(records); err != nil {
		return nil, "", err
	}

	userSetting, err := settingRepo.Get(repo.WithByKey("UserName"))
	if err != nil {
		return nil, "", err
	}
	sessionUser := psession.SessionUser{ID: psession.SuperAdminSessionUserID, Name: userSetting.Value, Role: "ADMIN"}
	res, err := auth.GenerateSession(c, sessionUser)
	if err != nil {
		return nil, "", err
	}
	if entrance != "" {
		SetSecurityEntranceCookie(c, entrance)
	}
	return res, "", nil
}

func SetSecurityEntranceCookie(c *gin.Context, entrance string) {
	entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance))
	sslEnabled := false
	if setting, err := settingRepo.Get(repo.WithByKey("SSL")); err == nil {
		sslEnabled = setting.Value == constant.StatusEnable
	}
	c.SetCookie("SecurityEntrance", entranceValue, 0, "/", "", sslEnabled, true)
}

func (u *AuthService) PasskeyBeginRegister(c *gin.Context, name string) (*dto.PasskeyBeginResponse, string, error) {
	config, msgKey, err := u.passkeyConfig(c)
	if err != nil {
		return nil, msgKey, err
	}
	records, err := loadPasskeyCredentialRecords()
	if err != nil {
		return nil, "", err
	}
	if len(records) >= passkey.PasskeyMaxCredentials {
		return nil, "ErrPasskeyLimit", buserr.New("ErrPasskeyLimit")
	}
	user, err := u.passkeyUser(records, true)
	if err != nil {
		return nil, "", err
	}

	wa, err := webauthn.New(config)
	if err != nil {
		return nil, "", err
	}
	exclusions := make([]protocol.CredentialDescriptor, len(user.Credentials))
	for i, credential := range user.Credentials {
		exclusions[i] = credential.Descriptor()
	}
	creation, sessionData, err := wa.BeginRegistration(user, webauthn.WithExclusions(exclusions))
	if err != nil {
		return nil, "", err
	}

	passkeySessions := passkey.GetPasskeySessionStore()
	sessionID := passkeySessions.Set(passkey.PasskeySessionKindRegister, strings.TrimSpace(name), *sessionData)
	return &dto.PasskeyBeginResponse{SessionID: sessionID, PublicKey: creation.Response}, "", nil
}

func (u *AuthService) PasskeyFinishRegister(c *gin.Context, sessionID string) (string, error) {
	if sessionID == "" {
		return "ErrPasskeySession", buserr.New("ErrPasskeySession")
	}
	config, msgKey, err := u.passkeyConfig(c)
	if err != nil {
		return msgKey, err
	}

	passkeySessions := passkey.GetPasskeySessionStore()
	session, ok := passkeySessions.Get(sessionID)
	if !ok || session.Kind != passkey.PasskeySessionKindRegister {
		return "ErrPasskeySession", buserr.New("ErrPasskeySession")
	}

	passkeySessions.Delete(sessionID)

	records, err := loadPasskeyCredentialRecords()
	if err != nil {
		return "", err
	}
	if len(records) >= passkey.PasskeyMaxCredentials {
		return "ErrPasskeyLimit", buserr.New("ErrPasskeyLimit")
	}

	user, err := u.passkeyUser(records, true)
	if err != nil {
		return "", err
	}

	wa, err := webauthn.New(config)
	if err != nil {
		return "", err
	}
	credential, err := wa.FinishRegistration(user, session.Session, c.Request)
	if err != nil {
		return "ErrPasskeyVerify", err
	}

	if passkeyCredentialExists(records, credential.ID) {
		return "ErrPasskeyDuplicate", buserr.New("ErrPasskeyDuplicate")
	}

	displayName := strings.TrimSpace(session.Name)
	if displayName == "" {
		displayName = fmt.Sprintf("%s-%s", passkey.PasskeyCredentialNameDefault, time.Now().Format("20060102150405"))
	}

	records = append(records, passkey.PasskeyCredentialRecord{
		ID:         base64.RawURLEncoding.EncodeToString(credential.ID),
		Name:       displayName,
		CreatedAt:  time.Now().Format(constant.DateTimeLayout),
		LastUsedAt: "",
		FlagsValue: credentialFlagsValue(credential.Flags),
		Credential: *credential,
	})

	if err := savePasskeyCredentialRecords(records); err != nil {
		return "", err
	}
	return "", nil
}

func (u *AuthService) PasskeyList() ([]dto.PasskeyInfo, error) {
	records, err := loadPasskeyCredentialRecords()
	if err != nil {
		return nil, err
	}
	list := make([]dto.PasskeyInfo, 0, len(records))
	for _, record := range records {
		list = append(list, dto.PasskeyInfo{
			ID:         record.ID,
			Name:       record.Name,
			CreatedAt:  record.CreatedAt,
			LastUsedAt: record.LastUsedAt,
		})
	}
	return list, nil
}

func (u *AuthService) PasskeyDelete(id string) error {
	records, err := loadPasskeyCredentialRecords()
	if err != nil {
		return err
	}
	index := -1
	for i, record := range records {
		if record.ID == id {
			index = i
			break
		}
	}
	if index == -1 {
		return buserr.New("ErrRecordNotFound")
	}
	records = append(records[:index], records[index+1:]...)
	return savePasskeyCredentialRecords(records)
}

func (u *AuthService) passkeyEnabled(c *gin.Context) (bool, error) {
	return strings.EqualFold(passkeyRequestScheme(c), "https"), nil
}

func (u *AuthService) passkeyConfigured() (bool, error) {
	bindDomain, err := settingRepo.Get(repo.WithByKey("BindDomain"))
	if err != nil {
		return false, err
	}
	if strings.TrimSpace(bindDomain.Value) == "" {
		return false, nil
	}
	records, err := loadPasskeyCredentialRecords()
	if err != nil {
		return false, err
	}
	return len(records) > 0, nil
}

func (u *AuthService) passkeyUser(records []passkey.PasskeyCredentialRecord, allowCreate bool) (*passkey.PasskeyUser, error) {
	userID, err := u.passkeyUserID(allowCreate)
	if err != nil {
		return nil, err
	}
	nameSetting, err := settingRepo.Get(repo.WithByKey("UserName"))
	if err != nil {
		return nil, err
	}
	credentials := make([]webauthn.Credential, len(records))
	for i, record := range records {
		credentials[i] = record.Credential
	}
	return &passkey.PasskeyUser{
		ID:          userID,
		Name:        nameSetting.Value,
		DisplayName: nameSetting.Value,
		Credentials: credentials,
	}, nil
}

func (u *AuthService) passkeyUserID(allowCreate bool) ([]byte, error) {
	setting, err := settingRepo.Get(repo.WithByKey(passkey.PasskeyUserIDSettingKey))
	if err != nil {
		return nil, err
	}
	if setting.Value == "" {
		if !allowCreate {
			return nil, buserr.New("ErrPasskeyNotConfigured")
		}
		raw := make([]byte, 32)
		if _, err := rand.Read(raw); err != nil {
			return nil, err
		}
		encoded := base64.RawURLEncoding.EncodeToString(raw)
		if err := settingRepo.Update(passkey.PasskeyUserIDSettingKey, encoded); err != nil {
			return nil, err
		}
		return raw, nil
	}
	raw, err := base64.RawURLEncoding.DecodeString(setting.Value)
	if err != nil {
		return nil, err
	}
	return raw, nil
}

func (u *AuthService) passkeyConfig(c *gin.Context) (*webauthn.Config, string, error) {
	enabled, err := u.passkeyEnabled(c)
	if err != nil {
		return nil, "", err
	}
	if !enabled {
		return nil, "ErrPasskeyDisabled", buserr.New("ErrPasskeyDisabled")
	}
	origin, rpID, err := passkeyOriginAndRPID(c)
	if err != nil {
		return nil, "", err
	}
	panelName, err := settingRepo.Get(repo.WithByKey("PanelName"))
	if err != nil {
		return nil, "", err
	}
	return &webauthn.Config{
		RPID:          rpID,
		RPDisplayName: panelName.Value,
		RPOrigins:     []string{origin},
		AuthenticatorSelection: protocol.AuthenticatorSelection{
			UserVerification: protocol.VerificationRequired,
		},
	}, "", nil
}

func (u *AuthService) checkEntrance(entrance string) error {
	entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
	if err != nil {
		return err
	}
	if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance {
		return buserr.New("ErrEntrance")
	}
	return nil
}

func loadPasskeyCredentialRecords() ([]passkey.PasskeyCredentialRecord, error) {
	setting, err := settingRepo.Get(repo.WithByKey(passkey.PasskeyCredentialSettingKey))
	if err != nil {
		return nil, err
	}
	if setting.Value == "" {
		return []passkey.PasskeyCredentialRecord{}, nil
	}
	decrypted, err := encrypt.StringDecrypt(setting.Value)
	if err != nil {
		return nil, err
	}
	var records []passkey.PasskeyCredentialRecord
	if err := json.Unmarshal([]byte(decrypted), &records); err != nil {
		return nil, err
	}
	for i := range records {
		records[i].Credential.Flags = webauthn.NewCredentialFlags(protocol.AuthenticatorFlags(records[i].FlagsValue))
	}
	return records, nil
}

func savePasskeyCredentialRecords(records []passkey.PasskeyCredentialRecord) error {
	if len(records) == 0 {
		return settingRepo.Update(passkey.PasskeyCredentialSettingKey, "")
	}
	for i := range records {
		records[i].FlagsValue = credentialFlagsValue(records[i].Credential.Flags)
	}
	raw, err := json.Marshal(records)
	if err != nil {
		return err
	}
	encrypted, err := encrypt.StringEncrypt(string(raw))
	if err != nil {
		return err
	}
	return settingRepo.Update(passkey.PasskeyCredentialSettingKey, encrypted)
}

func passkeyCredentialExists(records []passkey.PasskeyCredentialRecord, credentialID []byte) bool {
	encoded := base64.RawURLEncoding.EncodeToString(credentialID)
	for _, record := range records {
		if record.ID == encoded {
			return true
		}
	}
	return false
}

func updatePasskeyCredentialRecord(records []passkey.PasskeyCredentialRecord, credential *webauthn.Credential) error {
	encoded := base64.RawURLEncoding.EncodeToString(credential.ID)
	for i := range records {
		if records[i].ID == encoded {
			records[i].Credential = *credential
			records[i].FlagsValue = credentialFlagsValue(credential.Flags)
			records[i].LastUsedAt = time.Now().Format(constant.DateTimeLayout)
			return nil
		}
	}
	return buserr.New("ErrPasskeyNotConfigured")
}

func credentialFlagsValue(flags webauthn.CredentialFlags) uint8 {
	var value protocol.AuthenticatorFlags
	if flags.UserPresent {
		value |= protocol.FlagUserPresent
	}
	if flags.UserVerified {
		value |= protocol.FlagUserVerified
	}
	if flags.BackupEligible {
		value |= protocol.FlagBackupEligible
	}
	if flags.BackupState {
		value |= protocol.FlagBackupState
	}
	return uint8(value)
}

func passkeyOriginAndRPID(c *gin.Context) (string, string, error) {
	host := passkeyRequestHost(c)
	if host == "" {
		return "", "", fmt.Errorf("missing request host")
	}
	scheme := passkeyRequestScheme(c)
	origin := fmt.Sprintf("%s://%s", scheme, host)

	bindDomain, err := settingRepo.Get(repo.WithByKey("BindDomain"))
	if err != nil {
		return "", "", err
	}
	bindDomainValue := strings.TrimSpace(bindDomain.Value)
	if bindDomainValue == "" {
		return "", "", buserr.New("ErrPasskeyNotConfigured")
	}
	hostDomain := stripHostPort(host)
	bindDomainValue = stripHostPort(bindDomainValue)
	if hostDomain == "" || !strings.EqualFold(hostDomain, bindDomainValue) {
		return "", "", buserr.New("ErrPasskeyDisabled")
	}
	return origin, bindDomainValue, nil
}

func passkeyRequestHost(c *gin.Context) string {
	host := c.Request.Host
	if strings.Contains(host, ",") {
		host = strings.TrimSpace(strings.Split(host, ",")[0])
	}
	return strings.TrimSpace(host)
}

func passkeyRequestScheme(c *gin.Context) string {
	if c.Request.TLS != nil {
		return "https"
	}
	if !passkeyIsFromTrustedProxy(c) {
		return "http"
	}
	if proto := passkeyForwardedProto(c.GetHeader("Forwarded")); proto != "" {
		return proto
	}
	if proto := passkeyXForwardedProto(c.GetHeader("X-Forwarded-Proto")); proto != "" {
		return proto
	}
	return "http"
}

func passkeyIsFromTrustedProxy(c *gin.Context) bool {
	remoteIP := passkeyRemoteIP(c.Request.RemoteAddr)
	if remoteIP == nil {
		return false
	}
	proxies, err := loadPasskeyTrustedProxies()
	if err != nil {
		global.LOG.Errorf("load passkey trusted proxies failed, err: %v", err)
		return false
	}
	for _, cidr := range proxies {
		if cidr.Contains(remoteIP) {
			return true
		}
	}
	return false
}

func passkeyRemoteIP(remoteAddr string) net.IP {
	if host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr)); err == nil {
		return net.ParseIP(host)
	}
	return net.ParseIP(strings.TrimSpace(remoteAddr))
}

func loadPasskeyTrustedProxies() ([]*net.IPNet, error) {
	setting, err := settingRepo.Get(repo.WithByKey("PasskeyTrustedProxies"))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return parsePasskeyTrustedProxies("127.0.0.1\n::1")
		}
		return nil, err
	}
	return parsePasskeyTrustedProxies(setting.Value)
}

func parsePasskeyTrustedProxies(value string) ([]*net.IPNet, error) {
	lines := strings.Split(strings.TrimSpace(value), "\n")
	if len(lines) == 0 {
		return []*net.IPNet{}, nil
	}
	cidrs := make([]*net.IPNet, 0, len(lines))
	for _, line := range lines {
		item := strings.TrimSpace(line)
		if item == "" {
			continue
		}
		if ip := net.ParseIP(item); ip != nil {
			if ip.To4() != nil {
				item += "/32"
			} else {
				item += "/128"
			}
		}
		_, ipNet, err := net.ParseCIDR(item)
		if err != nil {
			return nil, err
		}
		cidrs = append(cidrs, ipNet)
	}
	return cidrs, nil
}

func passkeyForwardedProto(value string) string {
	if value == "" {
		return ""
	}
	for _, item := range strings.Split(value, ",") {
		for _, part := range strings.Split(item, ";") {
			kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
			if len(kv) != 2 {
				continue
			}
			if !strings.EqualFold(strings.TrimSpace(kv[0]), "proto") {
				continue
			}
			proto := strings.ToLower(strings.Trim(strings.TrimSpace(kv[1]), `"`))
			if proto == "http" || proto == "https" {
				return proto
			}
		}
	}
	return ""
}

func passkeyXForwardedProto(value string) string {
	if value == "" {
		return ""
	}
	proto := strings.ToLower(strings.TrimSpace(strings.Split(value, ",")[0]))
	if proto == "http" || proto == "https" {
		return proto
	}
	return ""
}

func stripHostPort(hostport string) string {
	if hostport == "" {
		return hostport
	}
	hostport = strings.TrimSpace(hostport)
	if host, _, err := net.SplitHostPort(hostport); err == nil {
		return strings.Trim(host, "[]")
	}
	return strings.Trim(hostport, "[]")
}
