// Copyright 2023 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package github

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
)

// PropertyValueType represents the type of a custom property value.
type PropertyValueType string

// Valid values for CustomProperty.ValueType.
const (
	PropertyValueTypeString       PropertyValueType = "string"
	PropertyValueTypeSingleSelect PropertyValueType = "single_select"
	PropertyValueTypeMultiSelect  PropertyValueType = "multi_select"
	PropertyValueTypeTrueFalse    PropertyValueType = "true_false"
	PropertyValueTypeURL          PropertyValueType = "url"
)

// CustomProperty represents an organization custom property object.
type CustomProperty struct {
	// PropertyName is required for most endpoints except when calling CreateOrUpdateCustomProperty;
	// where this is sent in the path and thus can be omitted.
	PropertyName *string `json:"property_name,omitempty"`
	// The URL that can be used to fetch, update, or delete info about this property via the API.
	URL *string `json:"url,omitempty"`
	// SourceType is the source type of the property where it has been created. Can be one of: organization, enterprise.
	SourceType *string `json:"source_type,omitempty"`
	// The type of the value for the property. Can be one of: string, single_select, multi_select, true_false, url.
	ValueType PropertyValueType `json:"value_type"`
	// Whether the property is required.
	Required *bool `json:"required,omitempty"`
	// Default value of the property.
	DefaultValue any `json:"default_value,omitempty"`
	// Short description of the property.
	Description *string `json:"description,omitempty"`
	// An ordered list of the allowed values of the property. The property can have up to 200
	// allowed values.
	AllowedValues []string `json:"allowed_values,omitempty"`
	// Who can edit the values of the property. Can be one of: org_actors, org_and_repo_actors, nil (null).
	ValuesEditableBy *string `json:"values_editable_by,omitempty"`
}

// DefaultValueString returns the DefaultValue as a string if the ValueType is string or single_select or url.
func (cp CustomProperty) DefaultValueString() (string, bool) {
	switch cp.ValueType {
	case PropertyValueTypeString, PropertyValueTypeSingleSelect, PropertyValueTypeURL:
		s, ok := cp.DefaultValue.(string)
		return s, ok
	default:
		return "", false
	}
}

// DefaultValueStrings returns the DefaultValue as a slice of string if the ValueType is multi_select.
func (cp CustomProperty) DefaultValueStrings() ([]string, bool) {
	switch cp.ValueType {
	case PropertyValueTypeMultiSelect:
		switch v := cp.DefaultValue.(type) {
		case []string:
			return v, true
		case []any:
			vals := make([]string, len(v))
			for i, item := range v {
				s, ok := item.(string)
				if !ok {
					return nil, false
				}
				vals[i] = s
			}
			return vals, true
		default:
			return nil, false
		}
	default:
		return nil, false
	}
}

// DefaultValueBool returns the DefaultValue as a string if the ValueType is true_false.
func (cp CustomProperty) DefaultValueBool() (bool, bool) {
	switch cp.ValueType {
	case PropertyValueTypeTrueFalse:
		if s, ok := cp.DefaultValue.(string); ok {
			b, err := strconv.ParseBool(s)
			return b, err == nil
		}
		return false, false
	default:
		return false, false
	}
}

// RepoCustomPropertyValue represents a repository custom property value.
type RepoCustomPropertyValue struct {
	RepositoryID       int64                  `json:"repository_id"`
	RepositoryName     string                 `json:"repository_name"`
	RepositoryFullName string                 `json:"repository_full_name"`
	Properties         []*CustomPropertyValue `json:"properties"`
}

// CustomPropertyValue represents a custom property value.
type CustomPropertyValue struct {
	PropertyName string `json:"property_name"`
	Value        any    `json:"value"`
}

// ListCustomPropertyValuesOptions specifies the optional parameters to the
// OrganizationsService.ListCustomPropertyValues method.
type ListCustomPropertyValuesOptions struct {
	RepositoryQuery string `url:"repository_query,omitempty"`
	ListOptions
}

// UnmarshalJSON implements the json.Unmarshaler interface.
// This helps us handle the fact that Value can be either a string, []string, or nil.
func (cpv *CustomPropertyValue) UnmarshalJSON(data []byte) error {
	type aliasCustomPropertyValue CustomPropertyValue
	aux := &struct {
		*aliasCustomPropertyValue
	}{
		aliasCustomPropertyValue: (*aliasCustomPropertyValue)(cpv),
	}
	if err := json.Unmarshal(data, &aux); err != nil {
		return err
	}

	switch v := aux.Value.(type) {
	case nil:
		cpv.Value = nil
	case string:
		cpv.Value = v
	case []any:
		strSlice := make([]string, len(v))
		for i, item := range v {
			str, ok := item.(string)
			if !ok {
				return errors.New("non-string value in string array")
			}
			strSlice[i] = str
		}
		cpv.Value = strSlice
	default:
		return fmt.Errorf("unexpected value type: %T", v)
	}
	return nil
}

// GetAllCustomProperties gets all custom properties that are defined for the specified organization.
//
// GitHub API docs: https://docs.github.com/rest/orgs/custom-properties#get-all-custom-properties-for-an-organization
//
//meta:operation GET /orgs/{org}/properties/schema
func (s *OrganizationsService) GetAllCustomProperties(ctx context.Context, org string) ([]*CustomProperty, *Response, error) {
	u := fmt.Sprintf("orgs/%v/properties/schema", org)

	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	var customProperties []*CustomProperty
	resp, err := s.client.Do(ctx, req, &customProperties)
	if err != nil {
		return nil, resp, err
	}

	return customProperties, resp, nil
}

// CreateOrUpdateCustomProperties creates new or updates existing custom properties that are defined for the specified organization.
//
// GitHub API docs: https://docs.github.com/rest/orgs/custom-properties#create-or-update-custom-properties-for-an-organization
//
//meta:operation PATCH /orgs/{org}/properties/schema
func (s *OrganizationsService) CreateOrUpdateCustomProperties(ctx context.Context, org string, properties []*CustomProperty) ([]*CustomProperty, *Response, error) {
	u := fmt.Sprintf("orgs/%v/properties/schema", org)

	params := struct {
		Properties []*CustomProperty `json:"properties"`
	}{
		Properties: properties,
	}

	req, err := s.client.NewRequest("PATCH", u, params)
	if err != nil {
		return nil, nil, err
	}

	var customProperties []*CustomProperty
	resp, err := s.client.Do(ctx, req, &customProperties)
	if err != nil {
		return nil, resp, err
	}

	return customProperties, resp, nil
}

// GetCustomProperty gets a custom property that is defined for the specified organization.
//
// GitHub API docs: https://docs.github.com/rest/orgs/custom-properties#get-a-custom-property-for-an-organization
//
//meta:operation GET /orgs/{org}/properties/schema/{custom_property_name}
func (s *OrganizationsService) GetCustomProperty(ctx context.Context, org, name string) (*CustomProperty, *Response, error) {
	u := fmt.Sprintf("orgs/%v/properties/schema/%v", org, name)

	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	var customProperty *CustomProperty
	resp, err := s.client.Do(ctx, req, &customProperty)
	if err != nil {
		return nil, resp, err
	}

	return customProperty, resp, nil
}

// CreateOrUpdateCustomProperty creates a new or updates an existing custom property that is defined for the specified organization.
//
// GitHub API docs: https://docs.github.com/rest/orgs/custom-properties#create-or-update-a-custom-property-for-an-organization
//
//meta:operation PUT /orgs/{org}/properties/schema/{custom_property_name}
func (s *OrganizationsService) CreateOrUpdateCustomProperty(ctx context.Context, org, customPropertyName string, property *CustomProperty) (*CustomProperty, *Response, error) {
	u := fmt.Sprintf("orgs/%v/properties/schema/%v", org, customPropertyName)

	req, err := s.client.NewRequest("PUT", u, property)
	if err != nil {
		return nil, nil, err
	}

	var customProperty *CustomProperty
	resp, err := s.client.Do(ctx, req, &customProperty)
	if err != nil {
		return nil, resp, err
	}

	return customProperty, resp, nil
}

// RemoveCustomProperty removes a custom property that is defined for the specified organization.
//
// GitHub API docs: https://docs.github.com/rest/orgs/custom-properties#remove-a-custom-property-for-an-organization
//
//meta:operation DELETE /orgs/{org}/properties/schema/{custom_property_name}
func (s *OrganizationsService) RemoveCustomProperty(ctx context.Context, org, customPropertyName string) (*Response, error) {
	u := fmt.Sprintf("orgs/%v/properties/schema/%v", org, customPropertyName)

	req, err := s.client.NewRequest("DELETE", u, nil)
	if err != nil {
		return nil, err
	}

	return s.client.Do(ctx, req, nil)
}

// ListCustomPropertyValues lists all custom property values for repositories in the specified organization.
//
// GitHub API docs: https://docs.github.com/rest/orgs/custom-properties#list-custom-property-values-for-organization-repositories
//
//meta:operation GET /orgs/{org}/properties/values
func (s *OrganizationsService) ListCustomPropertyValues(ctx context.Context, org string, opts *ListCustomPropertyValuesOptions) ([]*RepoCustomPropertyValue, *Response, error) {
	u := fmt.Sprintf("orgs/%v/properties/values", org)
	u, err := addOptions(u, opts)
	if err != nil {
		return nil, nil, err
	}

	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	var repoCustomPropertyValues []*RepoCustomPropertyValue
	resp, err := s.client.Do(ctx, req, &repoCustomPropertyValues)
	if err != nil {
		return nil, resp, err
	}

	return repoCustomPropertyValues, resp, nil
}

// CreateOrUpdateRepoCustomPropertyValues creates new or updates existing custom property values across multiple repositories for the specified organization.
//
// GitHub API docs: https://docs.github.com/rest/orgs/custom-properties#create-or-update-custom-property-values-for-organization-repositories
//
//meta:operation PATCH /orgs/{org}/properties/values
func (s *OrganizationsService) CreateOrUpdateRepoCustomPropertyValues(ctx context.Context, org string, repoNames []string, properties []*CustomPropertyValue) (*Response, error) {
	u := fmt.Sprintf("orgs/%v/properties/values", org)

	params := struct {
		RepositoryNames []string               `json:"repository_names"`
		Properties      []*CustomPropertyValue `json:"properties"`
	}{
		RepositoryNames: repoNames,
		Properties:      properties,
	}

	req, err := s.client.NewRequest("PATCH", u, params)
	if err != nil {
		return nil, err
	}

	return s.client.Do(ctx, req, nil)
}
