package jira

import (
	"bytes"
	"context"
	"encoding/base64"
	"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)
)

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

type Config struct {
	BaseURL  string
	Email    string
	APIToken string
}

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

// --- Jira-specific DTOS ---

type JiraIssueResponse struct {
	Key    string `json:"key"`
	Fields JiraFields
}

type JiraFields struct {
	Summary     string          `json:"summary"`
	Description json.RawMessage `json:"description"`
	Status      *JiraStatus     `json:"status"`
	IssueType   JiraIssueType   `json:"issuetype"`
	Labels      []string        `json:"labels"`
}

type JiraStatus struct {
	Name           string             `json:"name"`
	Color          string             `json:"color"`
	StatusCategory JiraStatusCategory `json:"statusCategory"`
}

type JiraStatusCategory struct {
	Name      string `json:"name"`
	ID        int    `json:"id"`
	Key       string `json:"key"`
	ColorName string `json:"colorName"`
}

type JiraIssueType struct {
	Name string `json:"name"`
}

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

type JiraCommentInput struct {
	Body json.RawMessage `json:"body"`
}

type JiraTransitionRequest struct {
	Transition JiraTransitionInput `json:"transition"`
}

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

type JiraTransitionResponse struct {
	Transitions []JiraTransitionOption `json:"transitions"`
}

type JiraTransitionOption struct {
	ID   string       `json:"id"`
	Name string       `json:"name"`
	To   JiraStatusTo `json:"to"`
}

type JiraStatusTo struct {
	Name string `json:"name"`
}

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("jira")
}

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

func (c *Client) FetchWorkItem(ctx context.Context, ref workitem.Ref) (workitem.WorkItem, error) {
	if err := c.validateConfig(); err != nil {
		return workitem.WorkItem{}, err
	}
	baseURL, _, _, _ := c.normalizedConfig()
	issueKey := strings.TrimSpace(ref.ID)
	if issueKey == "" {
		return workitem.WorkItem{}, ErrInvalidInput
	}

	endpoint := baseURL + "/rest/api/3/issue/" + url.PathEscape(issueKey)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return workitem.WorkItem{}, err
	}
	c.setAuthHeader(req)
	req.Header.Set("Accept", "application/json")

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

	raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return workitem.WorkItem{}, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return workitem.WorkItem{}, fmt.Errorf("jira API request failed (GET /rest/api/3/issue/%s): status %d: %s", issueKey, resp.StatusCode, strings.TrimSpace(string(raw)))
	}

	var jiraResp JiraIssueResponse
	if err := json.Unmarshal(raw, &jiraResp); err != nil {
		return workitem.WorkItem{}, fmt.Errorf("decode jira issue response: %w", err)
	}

	descText := extractDescriptionText(jiraResp.Fields.Description)
	statusName := ""
	if jiraResp.Fields.Status != nil {
		statusName = jiraResp.Fields.Status.Name
	}

	return workitem.WorkItem{
		Ref:             ref,
		Title:           jiraResp.Fields.Summary,
		DescriptionText: descText,
		DescriptionHTML: extractDescriptionHTML(jiraResp.Fields.Description),
		State:           statusName,
		Labels:          jiraResp.Fields.Labels,
	}, nil
}

func (c *Client) AppendComment(ctx context.Context, input workitem.CommentInput) error {
	if err := c.validateConfig(); err != nil {
		return err
	}
	baseURL, _, _, _ := c.normalizedConfig()
	issueKey := strings.TrimSpace(input.Ref.ID)
	if issueKey == "" {
		return ErrInvalidInput
	}

	body := convertCommentToADF(input.Body)
	commentReq := JiraCommentInput{Body: json.RawMessage(body)}

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(commentReq); err != nil {
		return err
	}

	endpoint := baseURL + "/rest/api/3/issue/" + url.PathEscape(issueKey) + "/comment"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, &buf)
	if err != nil {
		return err
	}
	c.setAuthHeader(req)
	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("jira API request failed (POST /rest/api/3/issue/%s/comment): status %d: %s", issueKey, resp.StatusCode, strings.TrimSpace(string(raw)))
	}
	return nil
}

func (c *Client) ProjectStatus(ctx context.Context, input workitem.StatusProjection) error {
	if err := c.validateConfig(); err != nil {
		return err
	}
	baseURL, _, _, _ := c.normalizedConfig()
	issueKey := strings.TrimSpace(input.Ref.ID)
	if issueKey == "" {
		return ErrInvalidInput
	}
	providerState := strings.TrimSpace(input.ProviderState)
	if providerState == "" {
		return ErrInvalidInput
	}

	transitionReq := JiraTransitionRequest{
		Transition: JiraTransitionInput{ID: providerState},
	}
	payload, err := json.Marshal(transitionReq)
	if err != nil {
		return err
	}

	endpoint := baseURL + "/rest/api/3/issue/" + url.PathEscape(issueKey) + "/transitions"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
	if err != nil {
		return err
	}
	c.setAuthHeader(req)
	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("jira API request failed (POST /rest/api/3/issue/%s/transitions): status %d: %s", issueKey, resp.StatusCode, strings.TrimSpace(string(raw)))
	}
	return nil
}

// --- Private helpers ---

func (c *Client) validateConfig() error {
	baseURL, _, _, err := c.normalizedConfig()
	if err != nil {
		return err
	}
	if _, err := url.ParseRequestURI(baseURL); err != nil {
		return err
	}
	return nil
}

func (c *Client) normalizedConfig() (string, string, string, error) {
	baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.BaseURL), "/")
	email := strings.TrimSpace(c.cfg.Email)
	token := strings.TrimSpace(c.cfg.APIToken)
	if baseURL == "" || email == "" || token == "" {
		return "", "", "", ErrMissingConfig
	}
	return baseURL, email, token, nil
}

func (c *Client) setAuthHeader(req *http.Request) {
	_, email, token, err := c.normalizedConfig()
	if err != nil {
		return
	}
	encoded := base64.StdEncoding.EncodeToString([]byte(email + ":" + token))
	req.Header.Set("Authorization", "Basic "+encoded)
}

// extractDescriptionText converts ADF description to plain text or falls back to the raw string.
func extractDescriptionText(raw json.RawMessage) string {
	if raw == nil || len(raw) == 0 {
		return ""
	}
	// Try parsing as ADF (Atlassian Document Format) first
	var adf struct {
		Type string `json:"type"`
	}
	if err := json.Unmarshal(raw, &adf); err == nil && adf.Type == "doc" {
		return adfToText(raw)
	}
	// Fallback: treat as plain string
	var s string
	if err := json.Unmarshal(raw, &s); err == nil {
		return s
	}
	// Fallback: treat as raw string in the response
	var v map[string]any
	if err := json.Unmarshal(raw, &v); err == nil {
		if val, ok := v["text"].(string); ok {
			return val
		}
	}
	return ""
}

// extractDescriptionHTML returns the ADF JSON string or plain text value.
func extractDescriptionHTML(raw json.RawMessage) string {
	if raw == nil || len(raw) == 0 {
		return ""
	}
	var adf struct {
		Type string `json:"type"`
	}
	if err := json.Unmarshal(raw, &adf); err == nil && adf.Type == "doc" {
		return string(raw)
	}
	var s string
	if err := json.Unmarshal(raw, &s); err == nil {
		return s
	}
	return ""
}

// adfToText converts ADF JSON to plain text by recursively walking nodes.
func adfToText(raw json.RawMessage) string {
	var doc struct {
		Content []map[string]any `json:"content"`
	}
	if err := json.Unmarshal(raw, &doc); err != nil {
		return ""
	}
	var parts []string
	for _, node := range doc.Content {
		extractText(node, &parts)
	}
	return strings.Join(parts, "\n")
}

func extractText(node map[string]any, parts *[]string) {
	if text, ok := node["text"].(string); ok {
		*parts = append(*parts, text)
		return
	}
	rawChildren, ok := node["content"].([]any)
	if !ok {
		return
	}
	for _, rc := range rawChildren {
		if child, ok := rc.(map[string]any); ok {
			extractText(child, parts)
		}
	}
}

// convertCommentToADF converts plain text to Jira ADF paragraph format with proper JSON escaping.
func convertCommentToADF(body string) string {
	trimmed := strings.TrimSpace(body)
	if trimmed == "" {
		return `{"type":"doc","version":1}`
	}
	rawJSON, _ := json.Marshal(trimmed)
	return fmt.Sprintf(`{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":%s}]}]}`, rawJSON)
}
