package notion

import (
	"encoding/json"
	"errors"
	"time"
)

// Database is a resource on the Notion platform.
// See: https://developers.notion.com/reference/database
type Database struct {
	ID             string             `json:"id"`
	CreatedTime    time.Time          `json:"created_time"`
	CreatedBy      BaseUser           `json:"created_by"`
	LastEditedTime time.Time          `json:"last_edited_time"`
	LastEditedBy   BaseUser           `json:"last_edited_by"`
	URL            string             `json:"url"`
	Title          []RichText         `json:"title"`
	Description    []RichText         `json:"description"`
	Properties     DatabaseProperties `json:"properties"`
	Parent         Parent             `json:"parent"`
	Icon           *Icon              `json:"icon,omitempty"`
	Cover          *Cover             `json:"cover,omitempty"`
	Archived       bool               `json:"archived"`
	IsInline       bool               `json:"is_inline"`
}

// DatabaseProperties is a mapping of properties defined on a database.
type DatabaseProperties map[string]DatabaseProperty

// Database property metadata types.
type (
	EmptyMetadata  struct{}
	NumberMetadata struct {
		Format NumberFormat `json:"format"`
	}
	SelectMetadata struct {
		Options []SelectOptions `json:"options"`
	}
	StatusMetadata struct {
		Options []SelectOptions `json:"options"`
		Groups  []StatusGroup   `json:"groups"`
	}
	FormulaMetadata struct {
		Expression string `json:"expression"`
	}
	RelationMetadata struct {
		DatabaseID string       `json:"database_id,omitempty"`
		Type       RelationType `json:"type,omitempty"`

		SingleProperty *struct{}             `json:"single_property,omitempty"`
		DualProperty   *DualPropertyRelation `json:"dual_property,omitempty"`
	}
	RollupMetadata struct {
		RelationPropName string         `json:"relation_property_name,omitempty"`
		RelationPropID   string         `json:"relation_property_id,omitempty"`
		RollupPropName   string         `json:"rollup_property_name,omitempty"`
		RollupPropID     string         `json:"rollup_property_id,omitempty"`
		Function         RollupFunction `json:"function,omitempty"`
	}
)

type DualPropertyRelation struct {
	SyncedPropID   string `json:"synced_property_id,omitempty"`
	SyncedPropName string `json:"synced_property_name,omitempty"`
}

type (
	RollupFunction string
	RelationType   string
)

const (
	RollupFunctionCountAll          RollupFunction = "count_all"
	RollupFunctionCountValues       RollupFunction = "count_values"
	RollupFunctionCountUniqueValues RollupFunction = "count_unique_values"
	RollupFunctionCountEmpty        RollupFunction = "count_empty"
	RollupFunctionCountNotEmpty     RollupFunction = "count_not_empty"
	RollupFunctionPercentEmpty      RollupFunction = "percent_empty"
	RollupFunctionPercentNotEmpty   RollupFunction = "percent_not_empty"
	RollupFunctionSum               RollupFunction = "sum"
	RollupFunctionAverage           RollupFunction = "average"
	RollupFunctionMedian            RollupFunction = "median"
	RollupFunctionMin               RollupFunction = "min"
	RollupFunctionMax               RollupFunction = "max"
	RollupFunctionRange             RollupFunction = "range"
	RollupFunctionShowOriginal      RollupFunction = "show_original"

	RelationTypeSingleProperty RelationType = "single_property"
	RelationTypeDualProperty   RelationType = "dual_property"
)

type SelectOptions struct {
	ID    string `json:"id,omitempty"`
	Name  string `json:"name,omitempty"`
	Color Color  `json:"color,omitempty"`
}

type StatusGroup struct {
	ID        string   `json:"id,omitempty"`
	Name      string   `json:"name,omitempty"`
	Color     Color    `json:"color,omitempty"`
	OptionIDs []string `json:"option_ids,omitempty"`
}

type FormulaResult struct {
	Type FormulaResultType `json:"type"`

	String  *string  `json:"string,omitempty"`
	Number  *float64 `json:"number,omitempty"`
	Boolean *bool    `json:"boolean,omitempty"`
	Date    *Date    `json:"date,omitempty"`
}

type Relation struct {
	ID string `json:"id"`
}

type RollupResult struct {
	Type RollupResultType `json:"type"`

	Number *float64               `json:"number,omitempty"`
	Date   *Date                  `json:"date,omitempty"`
	Array  []DatabasePageProperty `json:"array,omitempty"`
}

type People struct {
	People []User `json:"people"`
}

type File struct {
	Name string   `json:"name"`
	Type FileType `json:"type"`

	File     *FileFile     `json:"file,omitempty"`
	External *FileExternal `json:"external,omitempty"`
}

type DatabaseProperty struct {
	ID   string               `json:"id,omitempty"`
	Type DatabasePropertyType `json:"type"`
	Name string               `json:"name,omitempty"`

	Title          *EmptyMetadata `json:"title,omitempty"`
	RichText       *EmptyMetadata `json:"rich_text,omitempty"`
	Date           *EmptyMetadata `json:"date,omitempty"`
	People         *EmptyMetadata `json:"people,omitempty"`
	Files          *EmptyMetadata `json:"files,omitempty"`
	Checkbox       *EmptyMetadata `json:"checkbox,omitempty"`
	URL            *EmptyMetadata `json:"url,omitempty"`
	Email          *EmptyMetadata `json:"email,omitempty"`
	PhoneNumber    *EmptyMetadata `json:"phone_number,omitempty"`
	CreatedTime    *EmptyMetadata `json:"created_time,omitempty"`
	CreatedBy      *EmptyMetadata `json:"created_by,omitempty"`
	LastEditedTime *EmptyMetadata `json:"last_edited_time,omitempty"`
	LastEditedBy   *EmptyMetadata `json:"last_edited_by,omitempty"`

	Number      *NumberMetadata   `json:"number,omitempty"`
	Select      *SelectMetadata   `json:"select,omitempty"`
	MultiSelect *SelectMetadata   `json:"multi_select,omitempty"`
	Formula     *FormulaMetadata  `json:"formula,omitempty"`
	Relation    *RelationMetadata `json:"relation,omitempty"`
	Rollup      *RollupMetadata   `json:"rollup,omitempty"`
	Status      *StatusMetadata   `json:"status,omitempty"`
}

// DatabaseQuery is used for quering a database.
type DatabaseQuery struct {
	Filter      *DatabaseQueryFilter `json:"filter,omitempty"`
	Sorts       []DatabaseQuerySort  `json:"sorts,omitempty"`
	StartCursor string               `json:"start_cursor,omitempty"`
	PageSize    int                  `json:"page_size,omitempty"`
}

// DatabaseQueryResponse contains the results and pagination data from a query request.
type DatabaseQueryResponse struct {
	Results    []Page  `json:"results"`
	HasMore    bool    `json:"has_more"`
	NextCursor *string `json:"next_cursor"`
}

// DatabaseQueryFilter is used to filter database contents.
// See: https://developers.notion.com/reference/post-database-query#post-database-query-filter
type DatabaseQueryFilter struct {
	Property string `json:"property,omitempty"`

	DatabaseQueryPropertyFilter

	Timestamp Timestamp             `json:"timestamp,omitempty"`
	Or        []DatabaseQueryFilter `json:"or,omitempty"`
	And       []DatabaseQueryFilter `json:"and,omitempty"`
}

type DatabaseQueryPropertyFilter struct {
	Title       *TextPropertyFilter `json:"title,omitempty"`
	RichText    *TextPropertyFilter `json:"rich_text,omitempty"`
	URL         *TextPropertyFilter `json:"url,omitempty"`
	Email       *TextPropertyFilter `json:"email,omitempty"`
	PhoneNumber *TextPropertyFilter `json:"phone_number,omitempty"`

	Date           *DatePropertyFilter `json:"date,omitempty"`
	CreatedTime    *DatePropertyFilter `json:"created_time,omitempty"`
	LastEditedTime *DatePropertyFilter `json:"last_edited_time,omitempty"`

	Number      *NumberDatabaseQueryFilter      `json:"number,omitempty"`
	Checkbox    *CheckboxDatabaseQueryFilter    `json:"checkbox,omitempty"`
	Select      *SelectDatabaseQueryFilter      `json:"select,omitempty"`
	MultiSelect *MultiSelectDatabaseQueryFilter `json:"multi_select,omitempty"`
	Status      *StatusDatabaseQueryFilter      `json:"status,omitempty"`
	People      *PeopleDatabaseQueryFilter      `json:"people,omitempty"`
	Files       *FilesDatabaseQueryFilter       `json:"files,omitempty"`
	Relation    *RelationDatabaseQueryFilter    `json:"relation,omitempty"`
	Formula     *FormulaDatabaseQueryFilter     `json:"formula,omitempty"`
	Rollup      *RollupDatabaseQueryFilter      `json:"rollup,omitempty"`

	CreatedBy    *PeopleDatabaseQueryFilter `json:"created_by,omitempty"`
	LastEditedBy *PeopleDatabaseQueryFilter `json:"last_edited_by,omitempty"`
}

type Timestamp string

const (
	TimestampCreatedTime    = "created_time"
	TimestampLastEditedTime = "last_edited_time"
)

type TextPropertyFilter struct {
	Equals         string `json:"equals,omitempty"`
	DoesNotEqual   string `json:"does_not_equal,omitempty"`
	Contains       string `json:"contains,omitempty"`
	DoesNotContain string `json:"does_not_contain,omitempty"`
	StartsWith     string `json:"starts_with,omitempty"`
	EndsWith       string `json:"ends_with,omitempty"`
	IsEmpty        bool   `json:"is_empty,omitempty"`
	IsNotEmpty     bool   `json:"is_not_empty,omitempty"`
}

type NumberDatabaseQueryFilter struct {
	Equals               *int `json:"equals,omitempty"`
	DoesNotEqual         *int `json:"does_not_equal,omitempty"`
	GreaterThan          *int `json:"greater_than,omitempty"`
	LessThan             *int `json:"less_than,omitempty"`
	GreaterThanOrEqualTo *int `json:"greater_than_or_equal_to,omitempty"`
	LessThanOrEqualTo    *int `json:"less_than_or_equal_to,omitempty"`
	IsEmpty              bool `json:"is_empty,omitempty"`
	IsNotEmpty           bool `json:"is_not_empty,omitempty"`
}

type CheckboxDatabaseQueryFilter struct {
	Equals       *bool `json:"equals,omitempty"`
	DoesNotEqual *bool `json:"does_not_equal,omitempty"`
}

type SelectDatabaseQueryFilter struct {
	Equals       string `json:"equals,omitempty"`
	DoesNotEqual string `json:"does_not_equal,omitempty"`
	IsEmpty      bool   `json:"is_empty,omitempty"`
	IsNotEmpty   bool   `json:"is_not_empty,omitempty"`
}

type MultiSelectDatabaseQueryFilter struct {
	Contains       string `json:"contains,omitempty"`
	DoesNotContain string `json:"does_not_contain,omitempty"`
	IsEmpty        bool   `json:"is_empty,omitempty"`
	IsNotEmpty     bool   `json:"is_not_empty,omitempty"`
}

type StatusDatabaseQueryFilter struct {
	Equals       string `json:"equals,omitempty"`
	DoesNotEqual string `json:"does_not_equal,omitempty"`
	IsEmpty      bool   `json:"is_empty,omitempty"`
	IsNotEmpty   bool   `json:"is_not_empty,omitempty"`
}

type DatePropertyFilter struct {
	Equals     *time.Time `json:"equals,omitempty"`
	Before     *time.Time `json:"before,omitempty"`
	After      *time.Time `json:"after,omitempty"`
	OnOrBefore *time.Time `json:"on_or_before,omitempty"`
	OnOrAfter  *time.Time `json:"on_or_after,omitempty"`
	IsEmpty    bool       `json:"is_empty,omitempty"`
	IsNotEmpty bool       `json:"is_not_empty,omitempty"`
	PastWeek   *struct{}  `json:"past_week,omitempty"`
	PastMonth  *struct{}  `json:"past_month,omitempty"`
	PastYear   *struct{}  `json:"past_year,omitempty"`
	NextWeek   *struct{}  `json:"next_week,omitempty"`
	NextMonth  *struct{}  `json:"next_month,omitempty"`
	NextYear   *struct{}  `json:"next_year,omitempty"`
}

type PeopleDatabaseQueryFilter struct {
	Contains       string `json:"contains,omitempty"`
	DoesNotContain string `json:"does_not_contain,omitempty"`
	IsEmpty        bool   `json:"is_empty,omitempty"`
	IsNotEmpty     bool   `json:"is_not_empty,omitempty"`
}

type FilesDatabaseQueryFilter struct {
	IsEmpty    bool `json:"is_empty,omitempty"`
	IsNotEmpty bool `json:"is_not_empty,omitempty"`
}

type RelationDatabaseQueryFilter struct {
	Contains       string `json:"contains,omitempty"`
	DoesNotContain string `json:"does_not_contain,omitempty"`
	IsEmpty        bool   `json:"is_empty,omitempty"`
	IsNotEmpty     bool   `json:"is_not_empty,omitempty"`
}

type RollupDatabaseQueryFilter struct {
	Any    *DatabaseQueryPropertyFilter `json:"any,omitempty"`
	Every  *DatabaseQueryPropertyFilter `json:"every,omitempty"`
	None   *DatabaseQueryPropertyFilter `json:"none,omitempty"`
	Number *NumberDatabaseQueryFilter   `json:"number,omitempty"`
	Date   *DatePropertyFilter          `json:"date,omitempty"`
}

type FormulaDatabaseQueryFilter struct {
	String   *TextPropertyFilter          `json:"string,omitempty"`
	Checkbox *CheckboxDatabaseQueryFilter `json:"checkbox,omitempty"`
	Number   *NumberDatabaseQueryFilter   `json:"number,omitempty"`
	Date     *DatePropertyFilter          `json:"date,omitempty"`
}

type DatabaseQuerySort struct {
	Property  string        `json:"property,omitempty"`
	Timestamp SortTimestamp `json:"timestamp,omitempty"`
	Direction SortDirection `json:"direction,omitempty"`
}

// CreateDatabaseParams are the params used for creating a database.
type CreateDatabaseParams struct {
	ParentPageID string
	Title        []RichText
	Description  []RichText
	Properties   DatabaseProperties
	Icon         *Icon
	Cover        *Cover
	IsInline     bool
}

type (
	DatabasePropertyType string
	NumberFormat         string
	FormulaResultType    string
	RollupResultType     string
	SortTimestamp        string
	SortDirection        string
)

const (
	// Database property type enums.
	DBPropTypeTitle          DatabasePropertyType = "title"
	DBPropTypeRichText       DatabasePropertyType = "rich_text"
	DBPropTypeNumber         DatabasePropertyType = "number"
	DBPropTypeSelect         DatabasePropertyType = "select"
	DBPropTypeMultiSelect    DatabasePropertyType = "multi_select"
	DBPropTypeDate           DatabasePropertyType = "date"
	DBPropTypePeople         DatabasePropertyType = "people"
	DBPropTypeFiles          DatabasePropertyType = "files"
	DBPropTypeCheckbox       DatabasePropertyType = "checkbox"
	DBPropTypeURL            DatabasePropertyType = "url"
	DBPropTypeEmail          DatabasePropertyType = "email"
	DBPropTypePhoneNumber    DatabasePropertyType = "phone_number"
	DBPropTypeStatus         DatabasePropertyType = "status"
	DBPropTypeFormula        DatabasePropertyType = "formula"
	DBPropTypeRelation       DatabasePropertyType = "relation"
	DBPropTypeRollup         DatabasePropertyType = "rollup"
	DBPropTypeCreatedTime    DatabasePropertyType = "created_time"
	DBPropTypeCreatedBy      DatabasePropertyType = "created_by"
	DBPropTypeLastEditedTime DatabasePropertyType = "last_edited_time"
	DBPropTypeLastEditedBy   DatabasePropertyType = "last_edited_by"

	// Used for paginated property values.
	// See: https://developers.notion.com/reference/property-item-object#paginated-property-values
	DBPropTypePropertyItem DatabasePropertyType = "property_item"

	// Number format enums.
	NumberFormatNumber           NumberFormat = "number"
	NumberFormatNumberWithCommas NumberFormat = "number_with_commas"
	NumberFormatPercent          NumberFormat = "percent"
	NumberFormatDollar           NumberFormat = "dollar"
	NumberFormatEuro             NumberFormat = "euro"
	NumberFormatPound            NumberFormat = "pound"
	NumberFormatPonud            NumberFormat = "yen"
	NumberFormatRuble            NumberFormat = "ruble"
	NumberFormatRupee            NumberFormat = "rupee"
	NumberFormatWon              NumberFormat = "won"
	NumberFormatYuan             NumberFormat = "yuan"
	NumberFormatHongKongDollar   NumberFormat = "hong_kong_dollar"
	NumberFormatNewZealandDollar NumberFormat = "new_zealand_dollar"
	NumberFormatKrona            NumberFormat = "krona"
	NumberFormatNorwegianKrone   NumberFormat = "norwegian_krone"
	NumberFormatMexicanPeso      NumberFormat = "mexican_peso"
	NumberFormatRand             NumberFormat = "rand"
	NumberFormatNewTaiwanDollar  NumberFormat = "new_taiwan_dollar"
	NumberFormatDanishKrone      NumberFormat = "danish_krone"
	NumberFormatZloty            NumberFormat = "zloty"
	NumberFormatBaht             NumberFormat = "baht"
	NumberFormatForint           NumberFormat = "forint"
	NumberFormatKoruna           NumberFormat = "koruna"
	NumberFormatShekel           NumberFormat = "shekel"
	NumberFormatChileanPeso      NumberFormat = "chilean_peso"
	NumberFormatPhilippinePeso   NumberFormat = "philippine_peso"
	NumberFormatDirham           NumberFormat = "dirham"
	NumberFormatColombianPeso    NumberFormat = "colombian_peso"
	NumberFormatRiyal            NumberFormat = "riyal"
	NumberFormatRinggit          NumberFormat = "ringgit"
	NumberFormatLeu              NumberFormat = "leu"

	// Formula result type enums.
	FormulaResultTypeString  FormulaResultType = "string"
	FormulaResultTypeNumber  FormulaResultType = "number"
	FormulaResultTypeBoolean FormulaResultType = "boolean"
	FormulaResultTypeDate    FormulaResultType = "date"

	// Rollup result type enums.
	RollupResultTypeNumber      RollupResultType = "number"
	RollupResultTypeDate        RollupResultType = "date"
	RollupResultTypeArray       RollupResultType = "array"
	RollupResultTypeUnsupported RollupResultType = "unsupported"
	RollupResultTypeIncomplete  RollupResultType = "incomplete"

	// Sort timestamp enums.
	SortTimeStampCreatedTime    SortTimestamp = "created_time"
	SortTimeStampLastEditedTime SortTimestamp = "last_edited_time"

	// Sort direction enums.
	SortDirAsc  SortDirection = "ascending"
	SortDirDesc SortDirection = "descending"
)

// Metadata returns the underlying property metadata, based on its `type` field.
// When type is unknown/unmapped or doesn't have additional properies, `nil` is returned.
func (prop DatabaseProperty) Metadata() interface{} {
	switch prop.Type {
	case "title":
		return prop.Title
	case "number":
		return prop.Number
	case "select":
		return prop.Select
	case "multi_select":
		return prop.MultiSelect
	case "formula":
		return prop.Formula
	case "relation":
		return prop.Relation
	case "rollup":
		return prop.Rollup
	default:
		return nil
	}
}

// Value returns the underlying result value of an evaluated formula.
func (f FormulaResult) Value() interface{} {
	switch f.Type {
	case FormulaResultTypeString:
		return f.String
	case FormulaResultTypeNumber:
		return f.Number
	case FormulaResultTypeBoolean:
		return f.Boolean
	case FormulaResultTypeDate:
		return f.Date
	default:
		return nil
	}
}

// Value returns the underlying result value of an evaluated rollup.
func (r RollupResult) Value() interface{} {
	switch r.Type {
	case RollupResultTypeNumber:
		return r.Number
	case RollupResultTypeDate:
		return r.Date
	case RollupResultTypeArray:
		return r.Array
	default:
		return nil
	}
}

// Validate validates params for creating a database.
func (p CreateDatabaseParams) Validate() error {
	if p.ParentPageID == "" {
		return errors.New("parent page ID is required")
	}
	if p.Properties == nil {
		return errors.New("database properties are required")
	}
	if p.Icon != nil {
		if err := p.Icon.Validate(); err != nil {
			return err
		}
	}
	if p.Cover != nil {
		if err := p.Cover.Validate(); err != nil {
			return err
		}
	}

	return nil
}

// MarshalJSON implements json.Marshaler.
func (p CreateDatabaseParams) MarshalJSON() ([]byte, error) {
	type CreatePageParamsDTO struct {
		Parent      Parent             `json:"parent"`
		Title       []RichText         `json:"title,omitempty"`
		Description []RichText         `json:"description,omitempty"`
		Properties  DatabaseProperties `json:"properties"`
		Icon        *Icon              `json:"icon,omitempty"`
		Cover       *Cover             `json:"cover,omitempty"`
		IsInline    bool               `json:"is_inline,omitempty"`
	}

	parent := Parent{
		Type:   ParentTypePage,
		PageID: p.ParentPageID,
	}

	dto := CreatePageParamsDTO{
		Parent:      parent,
		Title:       p.Title,
		Description: p.Description,
		Properties:  p.Properties,
		Icon:        p.Icon,
		Cover:       p.Cover,
		IsInline:    p.IsInline,
	}

	return json.Marshal(dto)
}

// UpdateDatabaseParams are the params used for updating a database.
type UpdateDatabaseParams struct {
	Title       []RichText                   `json:"title,omitempty"`
	Description []RichText                   `json:"description,omitempty"`
	Properties  map[string]*DatabaseProperty `json:"properties,omitempty"`
	Icon        *Icon                        `json:"icon,omitempty"`
	Cover       *Cover                       `json:"cover,omitempty"`
	Archived    *bool                        `json:"archived,omitempty"`
	IsInline    *bool                        `json:"is_inline,omitempty"`
}

// Validate validates params for updating a database.
func (p UpdateDatabaseParams) Validate() error {
	if len(p.Title) == 0 && len(p.Properties) == 0 {
		return errors.New("either title or properties are required")
	}
	if p.Icon != nil {
		if err := p.Icon.Validate(); err != nil {
			return err
		}
	}
	if p.Cover != nil {
		if err := p.Cover.Validate(); err != nil {
			return err
		}
	}

	return nil
}