package types

import (
	"context"
	"net/url"
	"strings"
	"time"

	"github.com/SigNoz/signoz/pkg/errors"
	"github.com/SigNoz/signoz/pkg/valuer"
	"github.com/google/uuid"
	"github.com/uptrace/bun"
	"golang.org/x/crypto/bcrypt"
)

var (
	ErrUserAlreadyExists               = errors.MustNewCode("user_already_exists")
	ErrPasswordAlreadyExists           = errors.MustNewCode("password_already_exists")
	ErrUserNotFound                    = errors.MustNewCode("user_not_found")
	ErrResetPasswordTokenAlreadyExists = errors.MustNewCode("reset_password_token_already_exists")
	ErrPasswordNotFound                = errors.MustNewCode("password_not_found")
	ErrResetPasswordTokenNotFound      = errors.MustNewCode("reset_password_token_not_found")
	ErrAPIKeyAlreadyExists             = errors.MustNewCode("api_key_already_exists")
	ErrAPIKeyNotFound                  = errors.MustNewCode("api_key_not_found")
)

type UserStore interface {
	// invite
	CreateBulkInvite(ctx context.Context, invites []*Invite) error
	ListInvite(ctx context.Context, orgID string) ([]*Invite, error)
	DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
	GetInviteByToken(ctx context.Context, token string) (*GettableInvite, error)
	GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*Invite, error)

	// user
	CreateUserWithPassword(ctx context.Context, user *User, password *FactorPassword) (*User, error)
	CreateUser(ctx context.Context, user *User) error
	GetUserByID(ctx context.Context, orgID string, id string) (*GettableUser, error)
	GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*GettableUser, error)
	GetUsersByEmail(ctx context.Context, email string) ([]*GettableUser, error)
	GetUsersByRoleInOrg(ctx context.Context, orgID string, role Role) ([]*GettableUser, error)
	ListUsers(ctx context.Context, orgID string) ([]*GettableUser, error)
	UpdateUser(ctx context.Context, orgID string, id string, user *User) (*User, error)
	DeleteUser(ctx context.Context, orgID string, id string) error

	// password
	CreatePassword(ctx context.Context, password *FactorPassword) (*FactorPassword, error)
	CreateResetPasswordToken(ctx context.Context, resetPasswordRequest *ResetPasswordRequest) error
	GetPasswordByID(ctx context.Context, id string) (*FactorPassword, error)
	GetPasswordByUserID(ctx context.Context, id string) (*FactorPassword, error)
	GetResetPassword(ctx context.Context, token string) (*ResetPasswordRequest, error)
	GetResetPasswordByPasswordID(ctx context.Context, passwordID string) (*ResetPasswordRequest, error)
	UpdatePassword(ctx context.Context, userID string, password string) error
	UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, userID string, password string) error

	// Auth Domain
	GetDomainByName(ctx context.Context, name string) (*StorableOrgDomain, error)
	// org domain (auth domains) CRUD ops
	GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*GettableOrgDomain, error)
	ListDomains(ctx context.Context, orgId valuer.UUID) ([]*GettableOrgDomain, error)
	GetDomain(ctx context.Context, id uuid.UUID) (*GettableOrgDomain, error)
	CreateDomain(ctx context.Context, d *GettableOrgDomain) error
	UpdateDomain(ctx context.Context, domain *GettableOrgDomain) error
	DeleteDomain(ctx context.Context, id uuid.UUID) error

	// Temporary func for SSO
	GetDefaultOrgID(ctx context.Context) (string, error)

	// API KEY
	CreateAPIKey(ctx context.Context, apiKey *StorableAPIKey) error
	UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *StorableAPIKey, updaterID valuer.UUID) error
	ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*StorableAPIKeyUser, error)
	RevokeAPIKey(ctx context.Context, id valuer.UUID, revokedByUserID valuer.UUID) error
	GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*StorableAPIKeyUser, error)
}

type GettableUser struct {
	User
	Organization string `json:"organization"`
}

type User struct {
	bun.BaseModel `bun:"table:users"`

	Identifiable
	TimeAuditable
	DisplayName string `bun:"display_name,type:text,notnull" json:"displayName"`
	Email       string `bun:"email,type:text,notnull,unique:org_email" json:"email"`
	Role        string `bun:"role,type:text,notnull" json:"role"`
	OrgID       string `bun:"org_id,type:text,notnull,unique:org_email" json:"orgId"`
}

func NewUser(displayName string, email string, role string, orgID string) (*User, error) {
	if email == "" {
		return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
	}

	if role == "" {
		return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role is required")
	}

	if orgID == "" {
		return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
	}

	return &User{
		Identifiable: Identifiable{
			ID: valuer.GenerateUUID(),
		},
		TimeAuditable: TimeAuditable{
			CreatedAt: time.Now(),
		},
		DisplayName: displayName,
		Email:       email,
		Role:        role,
		OrgID:       orgID,
	}, nil
}

type PostableRegisterOrgAndAdmin struct {
	PostableAcceptInvite
	Name           string `json:"name"`
	OrgID          string `json:"orgId"`
	OrgDisplayName string `json:"orgDisplayName"`
	OrgName        string `json:"orgName"`
	Email          string `json:"email"`
}

type PostableAcceptInvite struct {
	DisplayName string `json:"displayName"`
	InviteToken string `json:"token"`
	Password    string `json:"password"`

	// reference URL to track where the register request is coming from
	SourceURL string `json:"sourceUrl"`
}

func (p *PostableAcceptInvite) Validate() error {
	if p.InviteToken == "" {
		return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required")
	}

	if p.Password == "" || len(p.Password) < 8 {
		return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password must be at least 8 characters long")
	}

	return nil
}

type FactorPassword struct {
	bun.BaseModel `bun:"table:factor_password"`

	Identifiable
	TimeAuditable
	Password  string `bun:"password,type:text,notnull" json:"password"`
	Temporary bool   `bun:"temporary,type:boolean,notnull" json:"temporary"`
	UserID    string `bun:"user_id,type:text,notnull,unique,references:user(id)" json:"userId"`
}

func NewFactorPassword(password string) (*FactorPassword, error) {

	if password == "" && len(password) < 8 {
		return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password must be at least 8 characters long")
	}

	password = strings.TrimSpace(password)

	hashedPassword, err := HashPassword(password)
	if err != nil {
		return nil, err
	}

	return &FactorPassword{
		Identifiable: Identifiable{
			ID: valuer.GenerateUUID(),
		},
		TimeAuditable: TimeAuditable{
			CreatedAt: time.Now(),
		},
		Password:  hashedPassword,
		Temporary: false,
	}, nil
}

func HashPassword(password string) (string, error) {
	// bcrypt automatically handles salting and uses a secure work factor
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	return string(hashedPassword), nil
}

func ComparePassword(hashedPassword, password string) bool {
	return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
}

type ResetPasswordRequest struct {
	bun.BaseModel `bun:"table:reset_password_token"`

	Identifiable
	Token      string `bun:"token,type:text,notnull" json:"token"`
	PasswordID string `bun:"password_id,type:text,notnull,unique" json:"passwordId"`
}

func NewResetPasswordRequest(passwordID string) (*ResetPasswordRequest, error) {
	return &ResetPasswordRequest{
		Identifiable: Identifiable{
			ID: valuer.GenerateUUID(),
		},
		Token:      valuer.GenerateUUID().String(),
		PasswordID: passwordID,
	}, nil
}

type PostableResetPassword struct {
	Password string `json:"password"`
	Token    string `json:"token"`
}

type ChangePasswordRequest struct {
	UserId      string `json:"userId"`
	OldPassword string `json:"oldPassword"`
	NewPassword string `json:"newPassword"`
}

type PostableLoginRequest struct {
	OrgID        string `json:"orgId"`
	Email        string `json:"email"`
	Password     string `json:"password"`
	RefreshToken string `json:"refreshToken"`
}

type GettableUserJwt struct {
	AccessJwt        string `json:"accessJwt"`
	AccessJwtExpiry  int64  `json:"accessJwtExpiry"`
	RefreshJwt       string `json:"refreshJwt"`
	RefreshJwtExpiry int64  `json:"refreshJwtExpiry"`
}

type GettableLoginResponse struct {
	GettableUserJwt
	UserID string `json:"userId"`
}

type GettableLoginPrecheck struct {
	SSO             bool     `json:"sso"`
	SSOUrl          string   `json:"ssoUrl"`
	CanSelfRegister bool     `json:"canSelfRegister"`
	IsUser          bool     `json:"isUser"`
	SSOError        string   `json:"ssoError"`
	SelectOrg       bool     `json:"selectOrg"`
	Orgs            []string `json:"orgs"`
}
