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

package lunatask

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

// Note is a note in Lunatask. Name and Content are encrypted client-side
// and will be null when read back from the API.
type Note struct {
	ID         string    `json:"id"`
	NotebookID *string   `json:"notebook_id"`
	DateOn     *Date     `json:"date_on"`
	Sources    []Source  `json:"sources"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

// CreateNoteRequest defines a new note.
// Use [NoteBuilder] for a fluent construction API.
type CreateNoteRequest struct {
	Name       *string `json:"name,omitempty"`
	Content    *string `json:"content,omitempty"`
	NotebookID *string `json:"notebook_id,omitempty"`
	Source     *string `json:"source,omitempty"`
	SourceID   *string `json:"source_id,omitempty"`
}

// UpdateNoteRequest specifies which fields to change on a note.
// Only non-nil fields are updated. Content replaces entirely (E2EE prevents appending).
type UpdateNoteRequest struct {
	Name       *string `json:"name,omitempty"`
	Content    *string `json:"content,omitempty"`
	NotebookID *string `json:"notebook_id,omitempty"`
	DateOn     *Date   `json:"date_on,omitempty"`
}

// noteResponse wraps a single note from the API.
type noteResponse struct {
	Note Note `json:"note"`
}

// notesResponse wraps a list of notes from the API.
type notesResponse struct {
	Notes []Note `json:"notes"`
}

// ListNotesOptions filters notes by source integration.
type ListNotesOptions struct {
	Source   *string
	SourceID *string
}

// ListNotes returns all notes, optionally filtered. Pass nil for all notes.
func (c *Client) ListNotes(ctx context.Context, opts *ListNotesOptions) ([]Note, error) {
	path := "/notes"

	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[notesResponse](ctx, c, http.MethodGet, path, nil)
	if err != nil {
		return nil, err
	}

	return resp.Notes, nil
}

// GetNote fetches a note by ID. Name and Content will be null (E2EE).
func (c *Client) GetNote(ctx context.Context, noteID string) (*Note, error) {
	if noteID == "" {
		return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
	}

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

	return &resp.Note, nil
}

// CreateNote adds a note. Returns (nil, nil) if a duplicate exists
// in the same notebook with matching source/source_id.
func (c *Client) CreateNote(ctx context.Context, note *CreateNoteRequest) (*Note, error) {
	resp, noContent, err := doJSON[noteResponse](ctx, c, http.MethodPost, "/notes", note)
	if err != nil {
		return nil, err
	}

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

	return &resp.Note, nil
}

// UpdateNote modifies a note. Only non-nil fields in the request are changed.
func (c *Client) UpdateNote(ctx context.Context, noteID string, note *UpdateNoteRequest) (*Note, error) {
	if noteID == "" {
		return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
	}

	resp, _, err := doJSON[noteResponse](ctx, c, http.MethodPut, "/notes/"+noteID, note)
	if err != nil {
		return nil, err
	}

	return &resp.Note, nil
}

// DeleteNote removes a note and returns its final state.
func (c *Client) DeleteNote(ctx context.Context, noteID string) (*Note, error) {
	if noteID == "" {
		return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
	}

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

	return &resp.Note, nil
}
