// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package lunatask

import (
	"context"
	"encoding/json"
	"fmt"
	"maps"
	"net/http"
	"net/url"
	"time"
)

// Person is a contact in Lunatask's relationship tracker.
// FirstName and LastName are encrypted client-side and will be null when read.
type Person struct {
	ID                   string                `json:"id"`
	RelationshipStrength *RelationshipStrength `json:"relationship_strength"`
	Sources              []Source              `json:"sources"`
	CreatedAt            time.Time             `json:"created_at"`
	UpdatedAt            time.Time             `json:"updated_at"`
}

// CreatePersonRequest defines a new person.
// Use [PersonBuilder] for a fluent construction API.
//
// CustomFields allows setting arbitrary custom fields. Lunatask supports
// "email", "birthday", and "phone" out of the box; other fields must first be
// defined in the app or [ErrUnprocessableEntity] is returned. These are
// flattened to top-level JSON fields when marshaled.
type CreatePersonRequest struct {
	FirstName            *string               `json:"first_name,omitempty"`
	LastName             *string               `json:"last_name,omitempty"`
	RelationshipStrength *RelationshipStrength `json:"relationship_strength,omitempty"`
	Source               *string               `json:"source,omitempty"`
	SourceID             *string               `json:"source_id,omitempty"`
	CustomFields         map[string]any        `json:"-"`
}

// MarshalJSON flattens CustomFields to top-level JSON fields.
func (r CreatePersonRequest) MarshalJSON() ([]byte, error) {
	// Alias to avoid infinite recursion
	type plain CreatePersonRequest

	base, err := json.Marshal(plain(r))
	if err != nil {
		return nil, fmt.Errorf("marshaling person request: %w", err)
	}

	if len(r.CustomFields) == 0 {
		return base, nil
	}

	// Merge custom fields into the base object
	var merged map[string]any
	if err := json.Unmarshal(base, &merged); err != nil {
		return nil, fmt.Errorf("unmarshaling person request for merge: %w", err)
	}

	maps.Copy(merged, r.CustomFields)

	data, err := json.Marshal(merged)
	if err != nil {
		return nil, fmt.Errorf("marshaling merged person request: %w", err)
	}

	return data, nil
}

// personResponse wraps a single person from the API.
type personResponse struct {
	Person Person `json:"person"`
}

// peopleResponse wraps a list of people from the API.
type peopleResponse struct {
	People []Person `json:"people"`
}

// ListPeopleOptions filters people by source integration.
type ListPeopleOptions struct {
	Source   *string
	SourceID *string
}

// ListPeople returns all people, optionally filtered. Pass nil for all.
func (c *Client) ListPeople(ctx context.Context, opts *ListPeopleOptions) ([]Person, error) {
	path := "/people"

	if opts != nil {
		params := url.Values{}
		if opts.Source != nil && *opts.Source != "" {
			params.Set("source", *opts.Source)
		}

		if opts.SourceID != nil && *opts.SourceID != "" {
			params.Set("source_id", *opts.SourceID)
		}

		if len(params) > 0 {
			path = fmt.Sprintf("%s?%s", path, params.Encode())
		}
	}

	resp, _, err := doJSON[peopleResponse](ctx, c, http.MethodGet, path, nil)
	if err != nil {
		return nil, err
	}

	return resp.People, nil
}

// GetPerson fetches a person by ID. Name fields will be null (E2EE).
func (c *Client) GetPerson(ctx context.Context, personID string) (*Person, error) {
	if personID == "" {
		return nil, fmt.Errorf("%w: person ID cannot be empty", ErrBadRequest)
	}

	resp, _, err := doJSON[personResponse](ctx, c, http.MethodGet, "/people/"+personID, nil)
	if err != nil {
		return nil, err
	}

	return &resp.Person, nil
}

// CreatePerson adds a person. Returns (nil, nil) if a duplicate exists
// with matching source/source_id.
func (c *Client) CreatePerson(ctx context.Context, person *CreatePersonRequest) (*Person, error) {
	resp, noContent, err := doJSON[personResponse](ctx, c, http.MethodPost, "/people", person)
	if err != nil {
		return nil, err
	}

	if noContent {
		// Intentional: duplicate exists (HTTP 204), not an error
		return nil, nil //nolint:nilnil
	}

	return &resp.Person, nil
}

// DeletePerson removes a person and returns their final state.
func (c *Client) DeletePerson(ctx context.Context, personID string) (*Person, error) {
	if personID == "" {
		return nil, fmt.Errorf("%w: person ID cannot be empty", ErrBadRequest)
	}

	resp, _, err := doJSON[personResponse](ctx, c, http.MethodDelete, "/people/"+personID, nil)
	if err != nil {
		return nil, err
	}

	return &resp.Person, nil
}
