package plane

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"strings"

	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

var (
	_ workitem.Provider        = (*Client)(nil)
	_ workitem.Reader          = (*Client)(nil)
	_ workitem.Commenter       = (*Client)(nil)
	_ workitem.StatusProjector = (*Client)(nil)
	_ workitem.Creator         = (*Client)(nil)
	_ workitem.BodyProjector   = (*Client)(nil)
)

var (
	ErrNotImplemented = errors.New("plane adapter method is not implemented")
	ErrMissingConfig  = errors.New("plane adapter requires base URL and token")
	ErrInvalidInput   = errors.New("invalid plane adapter input")
)

type Config struct {
	BaseURL string
	Token   string
}

type Client struct {
	cfg    Config
	http   *http.Client
	logger *slog.Logger
}

type WorkItemRef struct {
	WorkspaceSlug string
	ProjectID     string
	WorkItemID    string
}

type WorkItem struct {
	ID                  string          `json:"id"`
	Name                string          `json:"name"`
	Description         string          `json:"description"`
	DescriptionHTML     string          `json:"description_html"`
	DescriptionStripped string          `json:"description_stripped"`
	State               string          `json:"state_id"`
	StateObj            json.RawMessage `json:"state"`
	AssigneeIDs         []string        `json:"assignee_ids"`
	Assignees           json.RawMessage `json:"assignees"`
}

type CreateIssueInput struct {
	WorkspaceSlug   string
	ProjectID       string
	Title           string
	Description     string
	DescriptionHTML string
}

type AddCommentInput struct {
	Ref            WorkItemRef
	CommentHTML    string
	Access         string
	ExternalSource string
	ExternalID     string
}

type UpdateIssueStatusInput struct {
	Ref    WorkItemRef
	Status string
}

// UpdateWorkItemInput carries an additive PATCH of a Plane work item. Only
// non-empty fields are sent, so a blank Title/DescriptionHTML/Status never
// clears an existing value. Status maps to the Plane `state` field, the same
// field UpdateIssueStatus patches.
type UpdateWorkItemInput struct {
	Ref             WorkItemRef
	Title           string
	DescriptionHTML string
	Status          string
}

func NewClient(cfg Config, logger *slog.Logger) *Client {
	return &Client{cfg: cfg, http: http.DefaultClient, logger: logger}
}

func NewClientWithHTTPClient(cfg Config, httpClient *http.Client, logger *slog.Logger) *Client {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	return &Client{cfg: cfg, http: httpClient, logger: logger}
}

func (c *Client) ProviderID() workitem.ProviderID {
	return workitem.ProviderID("plane")
}

func (c *Client) Capabilities() workitem.Capabilities {
	return workitem.Capabilities{
		workitem.CapabilityLookup:           true,
		workitem.CapabilityComment:          true,
		workitem.CapabilityStatusProjection: true,
		workitem.CapabilityCreate:           true,
		workitem.CapabilityBodyProjection:   true,
		workitem.CapabilityLabelProjection:  false,
	}
}

func (c *Client) FetchWorkItem(ctx context.Context, ref workitem.Ref) (workitem.WorkItem, error) {
	planeRef, err := planeWorkItemRef(ref)
	if err != nil {
		return workitem.WorkItem{}, err
	}
	item, err := c.GetWorkItem(ctx, planeRef)
	if err != nil {
		return workitem.WorkItem{}, err
	}

	state := item.State
	if state == "" && len(item.StateObj) > 0 {
		var stateStr string
		if err := json.Unmarshal(item.StateObj, &stateStr); err == nil {
			state = stateStr
		} else {
			var stateStruct struct {
				ID string `json:"id"`
			}
			if err := json.Unmarshal(item.StateObj, &stateStruct); err == nil {
				state = stateStruct.ID
			}
		}
	}

	var assigneeIDs []string
	if len(item.AssigneeIDs) > 0 {
		assigneeIDs = item.AssigneeIDs
	} else if len(item.Assignees) > 0 {
		var listStr []string
		if err := json.Unmarshal(item.Assignees, &listStr); err == nil && len(listStr) > 0 && listStr[0] != "" {
			assigneeIDs = listStr
		} else {
			var listObj []struct {
				ID string `json:"id"`
			}
			var parsedIDs []string
			if err := json.Unmarshal(item.Assignees, &listObj); err == nil {
				for _, obj := range listObj {
					if obj.ID != "" {
						parsedIDs = append(parsedIDs, obj.ID)
					}
				}
			}
			if len(parsedIDs) > 0 {
				assigneeIDs = parsedIDs
			} else {
				var listNested []struct {
					Assignee struct {
						ID string `json:"id"`
					} `json:"assignee"`
				}
				if err := json.Unmarshal(item.Assignees, &listNested); err == nil {
					for _, obj := range listNested {
						if obj.Assignee.ID != "" {
							assigneeIDs = append(assigneeIDs, obj.Assignee.ID)
						}
					}
				}
			}
		}
	}

	return workitem.WorkItem{
		Ref:             ref,
		Title:           item.Name,
		DescriptionText: firstNonEmpty(item.DescriptionStripped, item.Description),
		DescriptionHTML: item.DescriptionHTML,
		State:           state,
		AssigneeIDs:     assigneeIDs,
	}, nil
}

func (c *Client) AppendComment(ctx context.Context, input workitem.CommentInput) error {
	planeRef, err := planeWorkItemRef(input.Ref)
	if err != nil {
		return err
	}
	return c.AddComment(ctx, AddCommentInput{
		Ref:            planeRef,
		CommentHTML:    input.Body,
		Access:         input.Visibility,
		ExternalSource: input.ExternalSource,
		ExternalID:     input.ExternalID,
	})
}

func (c *Client) ProjectStatus(ctx context.Context, input workitem.StatusProjection) error {
	planeRef, err := planeWorkItemRef(input.Ref)
	if err != nil {
		return err
	}
	if strings.TrimSpace(input.ProviderState) == "" {
		return ErrInvalidInput
	}
	return c.UpdateIssueStatus(ctx, UpdateIssueStatusInput{
		Ref:    planeRef,
		Status: input.ProviderState,
	})
}

func (c *Client) ProjectBody(ctx context.Context, input workitem.BodyProjection) error {
	planeRef, err := planeWorkItemRef(input.Ref)
	if err != nil {
		return err
	}
	if strings.TrimSpace(input.Title) == "" && strings.TrimSpace(input.DescriptionHTML) == "" {
		return ErrInvalidInput
	}
	return c.UpdateWorkItem(ctx, UpdateWorkItemInput{
		Ref:             planeRef,
		Title:           input.Title,
		DescriptionHTML: input.DescriptionHTML,
	})
}

func (c *Client) CreateWorkItem(ctx context.Context, input workitem.CreateInput) (workitem.CreateResult, error) {
	workspace := strings.TrimSpace(input.Tenant)
	project := strings.TrimSpace(input.Project)
	item, err := c.CreateIssue(ctx, CreateIssueInput{
		WorkspaceSlug:   workspace,
		ProjectID:       project,
		Title:           input.Title,
		Description:     input.DescriptionText,
		DescriptionHTML: input.DescriptionHTML,
	})
	if err != nil {
		return workitem.CreateResult{}, err
	}
	return workitem.CreateResult{
		Ref: workitem.Ref{
			Provider: c.ProviderID(),
			Tenant:   workspace,
			Project:  project,
			ID:       item.ID,
		},
	}, nil
}

func planeWorkItemRef(ref workitem.Ref) (WorkItemRef, error) {
	tenant := strings.TrimSpace(ref.Tenant)
	project := strings.TrimSpace(ref.Project)
	id := strings.TrimSpace(ref.ID)
	if tenant == "" || project == "" || id == "" {
		return WorkItemRef{}, ErrInvalidInput
	}
	return WorkItemRef{
		WorkspaceSlug: tenant,
		ProjectID:     project,
		WorkItemID:    id,
	}, nil
}

func firstNonEmpty(vals ...string) string {
	for _, v := range vals {
		if v != "" {
			return v
		}
	}
	return ""
}

func (c *Client) CreateIssue(ctx context.Context, input CreateIssueInput) (WorkItem, error) {
	title := strings.TrimSpace(input.Title)
	if title == "" {
		return WorkItem{}, ErrInvalidInput
	}
	path := workItemsPath(input.WorkspaceSlug, input.ProjectID)
	if path == "" {
		return WorkItem{}, ErrInvalidInput
	}

	body := map[string]any{
		"name": title,
	}
	if descHTML := firstNonEmpty(input.DescriptionHTML, input.Description); descHTML != "" {
		body["description_html"] = descHTML
	}

	var output WorkItem
	if err := c.do(ctx, http.MethodPost, path, body, &output); err != nil {
		return WorkItem{}, err
	}
	return output, nil
}

func (c *Client) GetWorkItem(ctx context.Context, ref WorkItemRef) (WorkItem, error) {
	var output WorkItem
	if err := c.do(ctx, http.MethodGet, workItemPath(ref), nil, &output); err != nil {
		return WorkItem{}, err
	}
	return output, nil
}

func (c *Client) AddComment(ctx context.Context, input AddCommentInput) error {
	body := map[string]any{
		"comment_html": input.CommentHTML,
	}
	if input.Access != "" {
		body["access"] = input.Access
	}
	if input.ExternalSource != "" {
		body["external_source"] = input.ExternalSource
	}
	if input.ExternalID != "" {
		body["external_id"] = input.ExternalID
	}
	return c.do(ctx, http.MethodPost, workItemPath(input.Ref)+"/comments/", body, nil)
}

func (c *Client) UpdateIssueStatus(ctx context.Context, input UpdateIssueStatusInput) error {
	return c.UpdateWorkItem(ctx, UpdateWorkItemInput{
		Ref:    input.Ref,
		Status: input.Status,
	})
}

// UpdateWorkItem PATCHes the work item with only the non-empty fields in input.
// name, description_html, and state are sent independently so a body/title
// update never disturbs the state, and a status move never disturbs the body.
// An input with no non-empty field is an invalid no-op patch.
func (c *Client) UpdateWorkItem(ctx context.Context, input UpdateWorkItemInput) error {
	body := map[string]string{}
	if title := strings.TrimSpace(input.Title); title != "" {
		body["name"] = title
	}
	if descHTML := strings.TrimSpace(input.DescriptionHTML); descHTML != "" {
		body["description_html"] = descHTML
	}
	if status := strings.TrimSpace(input.Status); status != "" {
		body["state"] = status
	}
	if len(body) == 0 {
		return ErrInvalidInput
	}
	return c.do(ctx, http.MethodPatch, workItemPath(input.Ref)+"/", body, nil)
}

func (c *Client) do(ctx context.Context, method, path string, input any, output any) error {
	baseURL, token, err := c.config()
	if err != nil {
		return err
	}
	if strings.TrimSpace(path) == "" {
		return ErrInvalidInput
	}

	endpoint := baseURL + path
	var body io.Reader
	if input != nil {
		raw, err := json.Marshal(input)
		if err != nil {
			return err
		}
		body = bytes.NewReader(raw)
	}

	req, err := http.NewRequestWithContext(ctx, method, endpoint, body)
	if err != nil {
		return err
	}
	req.Header.Set("X-Api-Key", token)
	if input != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Accept", "application/json")

	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("plane API request failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
	}
	if output == nil || len(raw) == 0 {
		return nil
	}
	if err := json.Unmarshal(raw, output); err != nil {
		return fmt.Errorf("decode plane API response: %w", err)
	}
	return nil
}

func (c *Client) config() (string, string, error) {
	baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.BaseURL), "/")
	token := strings.TrimSpace(c.cfg.Token)
	if baseURL == "" || token == "" {
		return "", "", ErrMissingConfig
	}
	if _, err := url.ParseRequestURI(baseURL); err != nil {
		return "", "", err
	}
	return baseURL, token, nil
}

func workItemPath(ref WorkItemRef) string {
	workspaceSlug := strings.TrimSpace(ref.WorkspaceSlug)
	projectID := strings.TrimSpace(ref.ProjectID)
	workItemID := strings.TrimSpace(ref.WorkItemID)
	if workspaceSlug == "" || projectID == "" || workItemID == "" {
		return ""
	}
	return fmt.Sprintf(
		"/api/v1/workspaces/%s/projects/%s/work-items/%s",
		url.PathEscape(workspaceSlug),
		url.PathEscape(projectID),
		url.PathEscape(workItemID),
	)
}

func workItemsPath(workspaceSlug, projectID string) string {
	workspaceSlug = strings.TrimSpace(workspaceSlug)
	projectID = strings.TrimSpace(projectID)
	if workspaceSlug == "" || projectID == "" {
		return ""
	}
	return fmt.Sprintf(
		"/api/v1/workspaces/%s/projects/%s/work-items/",
		url.PathEscape(workspaceSlug),
		url.PathEscape(projectID),
	)
}

func (c *Client) log(ctx context.Context, message string, args ...any) {
	_ = ctx
	if c.logger != nil {
		c.logger.Info(message, args...)
	}
}
