// Copyright 2021 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package model

import (
	"errors"
	"fmt"
	"regexp"
	"sort"
)

var (
	ErrSecretNameInvalid  = errors.New("invalid secret name")
	ErrSecretImageInvalid = errors.New("invalid secret image")
	ErrSecretValueInvalid = errors.New("invalid secret value")
	ErrSecretEventInvalid = errors.New("invalid secret event")
)

// SecretService defines a service for managing secrets.
type SecretService interface {
	SecretListPipeline(*Repo, *Pipeline, *ListOptions) ([]*Secret, error)
	// Repository secrets
	SecretFind(*Repo, string) (*Secret, error)
	SecretList(*Repo, *ListOptions) ([]*Secret, error)
	SecretCreate(*Repo, *Secret) error
	SecretUpdate(*Repo, *Secret) error
	SecretDelete(*Repo, string) error
	// Organization secrets
	OrgSecretFind(int64, string) (*Secret, error)
	OrgSecretList(int64, *ListOptions) ([]*Secret, error)
	OrgSecretCreate(int64, *Secret) error
	OrgSecretUpdate(int64, *Secret) error
	OrgSecretDelete(int64, string) error
	// Global secrets
	GlobalSecretFind(string) (*Secret, error)
	GlobalSecretList(*ListOptions) ([]*Secret, error)
	GlobalSecretCreate(*Secret) error
	GlobalSecretUpdate(*Secret) error
	GlobalSecretDelete(string) error
}

// SecretStore persists secret information to storage.
type SecretStore interface {
	SecretFind(*Repo, string) (*Secret, error)
	SecretList(*Repo, bool, *ListOptions) ([]*Secret, error)
	SecretCreate(*Secret) error
	SecretUpdate(*Secret) error
	SecretDelete(*Secret) error
	OrgSecretFind(int64, string) (*Secret, error)
	OrgSecretList(int64, *ListOptions) ([]*Secret, error)
	GlobalSecretFind(string) (*Secret, error)
	GlobalSecretList(*ListOptions) ([]*Secret, error)
	SecretListAll() ([]*Secret, error)
}

// Secret represents a secret variable, such as a password or token.
type Secret struct {
	ID     int64          `json:"id"              xorm:"pk autoincr 'secret_id'"`
	OrgID  int64          `json:"org_id"          xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_org_id'"`
	RepoID int64          `json:"repo_id"         xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_repo_id'"`
	Name   string         `json:"name"            xorm:"NOT NULL UNIQUE(s) INDEX 'secret_name'"`
	Value  string         `json:"value,omitempty" xorm:"TEXT 'secret_value'"`
	Images []string       `json:"images"          xorm:"json 'secret_images'"`
	Events []WebhookEvent `json:"events"          xorm:"json 'secret_events'"`
} //	@name Secret

// TableName return database table name for xorm
func (Secret) TableName() string {
	return "secrets"
}

// BeforeInsert will sort events before inserted into database
func (s *Secret) BeforeInsert() {
	s.Events = sortEvents(s.Events)
}

// Global secret.
func (s Secret) IsGlobal() bool {
	return s.RepoID == 0 && s.OrgID == 0
}

// Organization secret.
func (s Secret) IsOrganization() bool {
	return s.RepoID == 0 && s.OrgID != 0
}

// Repository secret.
func (s Secret) IsRepository() bool {
	return s.RepoID != 0 && s.OrgID == 0
}

// Match returns true if an image and event match the restricted list.
// Note that EventPullClosed are treated as EventPull.
func (s *Secret) Match(event WebhookEvent) bool {
	// if there is no filter set secret matches all webhook events
	if len(s.Events) == 0 {
		return true
	}
	// tread all pull events the same way
	if event == EventPullClosed {
		event = EventPull
	}
	// one match is enough
	for _, e := range s.Events {
		if e == event {
			return true
		}
	}
	// a filter is set but the webhook did not match it
	return false
}

var validDockerImageString = regexp.MustCompile(
	`^(` +
		`[\w\d\-_\.]+` + // hostname
		`(:\d+)?` + // optional port
		`/)?` + // optional hostname + port
		`([\w\d\-_\.][\w\d\-_\.\/]*/)?` + // optional url prefix
		`([\w\d\-_]+)` + // image name
		`(:[\w\d\-_]+)?` + // optional image tag
		`$`,
)

// Validate validates the required fields and formats.
func (s *Secret) Validate() error {
	for _, event := range s.Events {
		if err := ValidateWebhookEvent(event); err != nil {
			return errors.Join(err, ErrSecretEventInvalid)
		}
	}
	if len(s.Events) == 0 {
		return fmt.Errorf("%w: no event specified", ErrSecretEventInvalid)
	}

	for _, image := range s.Images {
		if len(image) == 0 {
			return fmt.Errorf("%w: empty image in images", ErrSecretImageInvalid)
		}
		if !validDockerImageString.MatchString(image) {
			return fmt.Errorf("%w: image '%s' do not match regexp '%s'", ErrSecretImageInvalid, image, validDockerImageString.String())
		}
	}

	switch {
	case len(s.Name) == 0:
		return fmt.Errorf("%w: empty name", ErrSecretNameInvalid)
	case len(s.Value) == 0:
		return fmt.Errorf("%w: empty value", ErrSecretValueInvalid)
	default:
		return nil
	}
}

// Copy makes a copy of the secret without the value.
func (s *Secret) Copy() *Secret {
	return &Secret{
		ID:     s.ID,
		OrgID:  s.OrgID,
		RepoID: s.RepoID,
		Name:   s.Name,
		Images: s.Images,
		Events: sortEvents(s.Events),
	}
}

func sortEvents(wel WebhookEventList) WebhookEventList {
	sort.Sort(wel)
	return wel
}
