package common

import (
	"context"
	"errors"
	"fmt"
	"regexp"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"

	log "github.com/authzed/spicedb/internal/logging"
	"github.com/authzed/spicedb/pkg/spiceerrors"
	"github.com/authzed/spicedb/pkg/tuple"
)

// SerializationError is returned when there's been a serialization
// error while performing a datastore operation
type SerializationError struct {
	error
}

func (err SerializationError) GRPCStatus() *status.Status {
	return spiceerrors.WithCodeAndDetails(
		err,
		codes.Aborted,
		spiceerrors.ForReason(
			v1.ErrorReason_ERROR_REASON_SERIALIZATION_FAILURE,
			map[string]string{},
		),
	)
}

func (err SerializationError) Unwrap() error {
	return err.error
}

// NewSerializationError creates a new SerializationError
func NewSerializationError(err error) error {
	return SerializationError{err}
}

type WriteOverLimitError struct {
	error
}

func (err WriteOverLimitError) GRPCStatus() *status.Status {
	return spiceerrors.WithCodeAndDetails(
		err,
		codes.Aborted,
		spiceerrors.ForReason(
			v1.ErrorReason_ERROR_REASON_TOO_MANY_UPDATES_IN_REQUEST,
			map[string]string{},
		),
	)
}

func (err WriteOverLimitError) Unwrap() error {
	return err.error
}

// NewWriteOverLimitError creates a new WriteOverLimitError.
func NewWriteOverLimitError(err error) error {
	return WriteOverLimitError{err}
}

// ReadOnlyTransactionError is returned when an otherwise read-write
// transaction fails on writes with an error indicating that the datastore
// is currently in a read-only mode.
type ReadOnlyTransactionError struct {
	error
}

func (err ReadOnlyTransactionError) GRPCStatus() *status.Status {
	return spiceerrors.WithCodeAndDetails(
		err,
		codes.Aborted,
		spiceerrors.ForReason(
			v1.ErrorReason_ERROR_REASON_SERVICE_READ_ONLY,
			map[string]string{},
		),
	)
}

// NewReadOnlyTransactionError creates a new ReadOnlyTransactionError.
func NewReadOnlyTransactionError(err error) error {
	return ReadOnlyTransactionError{
		fmt.Errorf("could not perform write operation, as the datastore is currently in read-only mode: %w. This may indicate that the datastore has been put into maintenance mode", err),
	}
}

// CreateRelationshipExistsError is an error returned when attempting to CREATE an already-existing
// relationship.
type CreateRelationshipExistsError struct {
	error

	// Relationship is the relationship that caused the error. May be nil, depending on the datastore.
	Relationship *tuple.Relationship
}

// GRPCStatus implements retrieving the gRPC status for the error.
func (err CreateRelationshipExistsError) GRPCStatus() *status.Status {
	if err.Relationship == nil {
		return spiceerrors.WithCodeAndDetails(
			err,
			codes.AlreadyExists,
			spiceerrors.ForReason(
				v1.ErrorReason_ERROR_REASON_ATTEMPT_TO_RECREATE_RELATIONSHIP,
				map[string]string{},
			),
		)
	}

	relationship := tuple.ToV1Relationship(*err.Relationship)
	return spiceerrors.WithCodeAndDetails(
		err,
		codes.AlreadyExists,
		spiceerrors.ForReason(
			v1.ErrorReason_ERROR_REASON_ATTEMPT_TO_RECREATE_RELATIONSHIP,
			map[string]string{
				"relationship":       tuple.V1StringRelationshipWithoutCaveatOrExpiration(relationship),
				"resource_type":      relationship.Resource.ObjectType,
				"resource_object_id": relationship.Resource.ObjectId,
				"resource_relation":  relationship.Relation,
				"subject_type":       relationship.Subject.Object.ObjectType,
				"subject_object_id":  relationship.Subject.Object.ObjectId,
				"subject_relation":   relationship.Subject.OptionalRelation,
			},
		),
	)
}

// NewCreateRelationshipExistsError creates a new CreateRelationshipExistsError.
func NewCreateRelationshipExistsError(relationship *tuple.Relationship) error {
	msg := "could not CREATE one or more relationships, as they already existed. If this is persistent, please switch to TOUCH operations or specify a precondition"
	if relationship != nil {
		msg = fmt.Sprintf("could not CREATE relationship `%s`, as it already existed. If this is persistent, please switch to TOUCH operations or specify a precondition", tuple.StringWithoutCaveatOrExpiration(*relationship))
	}

	return CreateRelationshipExistsError{
		errors.New(msg),
		relationship,
	}
}

var (
	portMatchRegex  = regexp.MustCompile("invalid port \\\"(.+)\\\" after host")
	parseMatchRegex = regexp.MustCompile("parse \\\"(.+)\\\":")
)

// RedactAndLogSensitiveConnString elides the given error, logging it only at trace
// level (after being redacted).
func RedactAndLogSensitiveConnString(ctx context.Context, baseErr string, err error, pgURL string) error {
	if err == nil {
		return errors.New(baseErr)
	}

	// See: https://github.com/jackc/pgx/issues/1271
	filtered := err.Error()
	filtered = strings.ReplaceAll(filtered, pgURL, "(redacted)")
	filtered = portMatchRegex.ReplaceAllString(filtered, "(redacted)")
	filtered = parseMatchRegex.ReplaceAllString(filtered, "(redacted)")
	log.Ctx(ctx).Trace().Msg(baseErr + ": " + filtered)
	return fmt.Errorf("%s. To view details of this error (that may contain sensitive information), please run with --log-level=trace", baseErr)
}

// RevisionUnavailableError is returned when a revision is not available on a replica.
type RevisionUnavailableError struct {
	error
}

// NewRevisionUnavailableError creates a new RevisionUnavailableError.
func NewRevisionUnavailableError(err error) error {
	return RevisionUnavailableError{err}
}
