package protocol

import (
	"bytes"
	"context"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"time"

	"github.com/go-viper/mapstructure/v2"
	"github.com/golang-jwt/jwt/v5"

	"github.com/go-webauthn/webauthn/metadata"
)

// attestationFormatValidationHandlerAndroidSafetyNet is the handler for the Android SafetyNet Attestation Statement
// Format.
//
// When the authenticator is a platform authenticator on certain Android platforms, the attestation statement may be
// based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of the
// SafetyNet API (typically an application running on the Android platform) and the attestation statement provides some
// statements about the health of the platform and the identity of the calling application (see SafetyNet Documentation
// for more details).
//
// The syntax of an Android Attestation statement is defined as follows:
//
//	$$attStmtType //= (
//			fmt: "android-safetynet",
//			attStmt: safetynetStmtFormat
//	)
//
//	safetynetStmtFormat = {
//			ver: text,
//			response: bytes
//	}
//
// Specification: §8.5. Android SafetyNet Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-android-safetynet-attestation
//
//nolint:gocyclo
func attestationFormatValidationHandlerAndroidSafetyNet(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error) {
	// The syntax of an Android Attestation statement is defined as follows:
	//     $$attStmtType //= (
	//                           fmt: "android-safetynet",
	//                           attStmt: safetynetStmtFormat
	//                       )

	//     safetynetStmtFormat = {
	//                               ver: text,
	//                               response: bytes
	//                           }

	// §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
	// the contained fields.

	// We have done this
	// §8.5.2 Verify that response is a valid SafetyNet response of version ver.
	version, present := att.AttStatement[stmtVersion].(string)
	if !present {
		return "", nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
	}

	if version == "" {
		return "", nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet")
	}

	// TODO: provide user the ability to designate their supported versions.

	response, present := att.AttStatement["response"].([]byte)
	if !present {
		return "", nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
	}

	var token *jwt.Token

	if token, err = jwt.Parse(string(response), keyFuncSafetyNetJWT, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})); err != nil {
		return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
	}

	// marshall the JWT payload into the safetynet response json.
	var safetyNetResponse SafetyNetResponse

	if err = mapstructure.Decode(token.Claims, &safetyNetResponse); err != nil {
		return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err)).WithError(err)
	}

	// §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
	// of authenticatorData and clientDataHash.
	nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...))

	nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
	if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
		return "", nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response").WithError(err)
	}

	// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
	certChain, ok := token.Header[stmtX5C].([]any)
	if !ok || len(certChain) == 0 {
		return "", nil, ErrInvalidAttestation.WithDetails("Error getting certificate from JWT header x5c")
	}

	first, ok := certChain[0].(string)
	if !ok || first == "" {
		return "", nil, ErrInvalidAttestation.WithDetails("Error getting first certificate from JWT header x5c")
	}

	l := make([]byte, base64.StdEncoding.DecodedLen(len(first)))

	n, err := base64.StdEncoding.Decode(l, []byte(first))
	if err != nil {
		return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
	}

	attestationCert, err := x509.ParseCertificate(l[:n])
	if err != nil {
		return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
	}

	// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com".
	if err = attestationCert.VerifyHostname(attStatementAndroidSafetyNetHostname); err != nil {
		return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
	}

	// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
	if !safetyNetResponse.CtsProfileMatch {
		return "", nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
	}

	if t := time.Unix(safetyNetResponse.TimestampMs/1000, 0); t.After(time.Now()) {
		// Zero tolerance for post-dated timestamps.
		return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
	} else if t.Before(time.Now().Add(-time.Minute)) {
		// Small tolerance for pre-dated timestamps.
		if mds != nil && mds.GetValidateEntry(context.Background()) {
			return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp before one minute ago")
		}
	}

	// §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
	// trust path attestationCert.
	return string(metadata.BasicFull), nil, nil
}

func keyFuncSafetyNetJWT(token *jwt.Token) (key any, err error) {
	var (
		ok    bool
		raw   any
		chain []any
		first string
		der   []byte
		cert  *x509.Certificate
	)

	if raw, ok = token.Header[stmtX5C]; !ok {
		return nil, fmt.Errorf("jwt header missing x5c")
	}

	if chain, ok = raw.([]any); !ok || len(chain) == 0 {
		return nil, fmt.Errorf("jwt header x5c is not a non-empty array")
	}

	if first, ok = chain[0].(string); !ok || first == "" {
		return nil, fmt.Errorf("jwt header x5c[0] not a base64 string")
	}

	if der, err = base64.StdEncoding.DecodeString(first); err != nil {
		return nil, fmt.Errorf("decode x5c leaf: %w", err)
	}

	if cert, err = x509.ParseCertificate(der); err != nil {
		if cert != nil {
			return cert.PublicKey, fmt.Errorf("parse x5c leaf: %w", err)
		}

		return nil, fmt.Errorf("parse x5c leaf: %w", err)
	}

	return cert.PublicKey, nil
}

type SafetyNetResponse struct {
	Nonce                      string `json:"nonce"`
	TimestampMs                int64  `json:"timestampMs"`
	ApkPackageName             string `json:"apkPackageName"`
	ApkDigestSha256            string `json:"apkDigestSha256"`
	CtsProfileMatch            bool   `json:"ctsProfileMatch"`
	ApkCertificateDigestSha256 []any  `json:"apkCertificateDigestSha256"`
	BasicIntegrity             bool   `json:"basicIntegrity"`
}

func init() {
	RegisterAttestationFormat(AttestationFormatAndroidSafetyNet, attestationFormatValidationHandlerAndroidSafetyNet)
}
