// Copyright 2023 Harness, 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 types

import (
	"bytes"
	"encoding/json"
	"fmt"

	"github.com/harness/gitness/types/enum"

	"gopkg.in/yaml.v3"
)

type Rule struct {
	ID      int64 `json:"-"`
	Version int64 `json:"-"`

	CreatedBy int64 `json:"-"`
	Created   int64 `json:"created"`
	Updated   int64 `json:"updated"`

	RepoID  *int64 `json:"-"`
	SpaceID *int64 `json:"-"`

	Identifier  string `json:"identifier"`
	Description string `json:"description"`

	Type  RuleType       `json:"type"`
	State enum.RuleState `json:"state"`

	Pattern    json.RawMessage `json:"pattern"`
	Definition json.RawMessage `json:"definition"`

	CreatedByInfo PrincipalInfo `json:"created_by"`

	Users      map[int64]*PrincipalInfo `json:"users"`
	UserGroups map[int64]*UserGroupInfo `json:"user_groups"`

	// scope 0 indicates repo; scope > 0 indicates space depth level
	Scope int64 `json:"scope"`
}

// TODO [CODE-1363]: remove after identifier migration.
func (r Rule) MarshalJSON() ([]byte, error) {
	// alias allows us to embed the original object while avoiding an infinite loop of marshaling.
	type alias Rule
	return json.Marshal(&struct {
		alias
		UID string `json:"uid"`
	}{
		alias: (alias)(r),
		UID:   r.Identifier,
	})
}

func (r Rule) MarshalYAML() (interface{}, error) {
	// yaml cannot marshal json.RawMessage
	pattern := make(map[string]any)
	err := yaml.Unmarshal(r.Pattern, pattern)
	if err != nil {
		return nil, err
	}
	definition := make(map[string]any)
	err = yaml.Unmarshal(r.Definition, definition)
	if err != nil {
		return nil, err
	}
	return map[string]any{
		"id":          r.ID,
		"created":     r.Created,
		"updated":     r.Updated,
		"created_by":  r.CreatedBy,
		"identifier":  r.Identifier,
		"description": r.Description,
		"type":        r.Type,
		"state":       r.State,
		"pattern":     pattern,
		"definition":  definition,
	}, nil
}

// Clone makes deep copy of the rule object.
func (r Rule) Clone() Rule {
	var repoID *int64
	var spaceID *int64

	if r.RepoID != nil {
		id := *r.RepoID
		repoID = &id
	}

	if r.SpaceID != nil {
		id := *r.SpaceID
		spaceID = &id
	}

	r.RepoID = repoID
	r.SpaceID = spaceID

	pattern := make(json.RawMessage, len(r.Pattern))
	copy(pattern, r.Pattern)
	r.Pattern = pattern

	definition := make(json.RawMessage, len(r.Definition))
	copy(definition, r.Definition)
	r.Definition = definition

	users := make(map[int64]*PrincipalInfo, len(r.Users))
	for key, value := range r.Users {
		cloned := *value
		users[key] = &cloned
	}
	r.Users = users

	return r
}

func (r *Rule) IsEqual(rule *Rule) bool {
	return r.Identifier == rule.Identifier && r.State == rule.State &&
		r.Description == rule.Description && bytes.Equal(r.Pattern, rule.Pattern) &&
		bytes.Equal(r.Definition, rule.Definition)
}

type RuleType string

type RuleFilter struct {
	ListQueryFilter
	States []enum.RuleState
	Sort   enum.RuleSort `json:"sort"`
	Order  enum.Order    `json:"order"`
}

// Violation represents a single violation.
type Violation struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Params  []any  `json:"params"`
}

// RuleViolations holds several violations of a rule.
type RuleViolations struct {
	Rule       RuleInfo    `json:"rule"`
	Bypassable bool        `json:"bypassable"`
	Bypassed   bool        `json:"bypassed"`
	Violations []Violation `json:"violations"`
}

func (violations *RuleViolations) Add(code, message string) {
	violations.Violations = append(violations.Violations, Violation{
		Code:    code,
		Message: message,
		Params:  nil,
	})
}

func (violations *RuleViolations) Addf(code, format string, params ...any) {
	violations.Violations = append(violations.Violations, Violation{
		Code:    code,
		Message: fmt.Sprintf(format, params...),
		Params:  params,
	})
}

func (violations *RuleViolations) IsCritical() bool {
	return violations.Rule.State == enum.RuleStateActive && len(violations.Violations) > 0 && !violations.Bypassed
}

func (violations *RuleViolations) IsBypassed() bool {
	return violations.Rule.State == enum.RuleStateActive && len(violations.Violations) > 0 && violations.Bypassed
}

// RuleInfo holds basic info about a rule that is used to describe the rule in RuleViolations.
type RuleInfo struct {
	SpacePath string `json:"space_path,omitempty"`
	RepoPath  string `json:"repo_path,omitempty"`

	ID         int64          `json:"-"`
	Identifier string         `json:"identifier"`
	Type       RuleType       `json:"type"`
	State      enum.RuleState `json:"state"`
}

// TODO [CODE-1363]: remove after identifier migration.
func (r RuleInfo) MarshalJSON() ([]byte, error) {
	// alias allows us to embed the original object while avoiding an infinite loop of marshaling.
	type alias RuleInfo
	return json.Marshal(&struct {
		alias
		UID string `json:"uid"`
	}{
		alias: (alias)(r),
		UID:   r.Identifier,
	})
}

type RuleInfoInternal struct {
	RuleInfo
	Pattern    json.RawMessage
	Definition json.RawMessage
}

type RulesViolations struct {
	Message    string           `json:"message"`
	Violations []RuleViolations `json:"violations"`
}

type DryRunRulesOutput struct {
	DryRunRules    bool             `json:"dry_run_rules,omitempty"`
	RuleViolations []RuleViolations `json:"rule_violations,omitempty"`
}

type RuleParentInfo struct {
	Type enum.RuleParent
	ID   int64
}
