package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

type Ticket struct {
	Key         string    `json:"key"`
	Summary     string    `json:"summary"`
	Status      string    `json:"status"`
	Priority    string    `json:"priority"`
	Assignee    string    `json:"assignee"`
	Reporter    string    `json:"reporter"`
	Created     time.Time `json:"created"`
	Updated     time.Time `json:"updated"`
	Description string    `json:"description"`
	Comments    []Comment `json:"comments,omitempty"`
	URL         string    `json:"url"`
}

type Comment struct {
	Author  string `json:"author"`
	Created string `json:"created"`
	Body    string `json:"body"`
}

type jiraSearchResp struct {
	Issues []struct {
		Key    string `json:"key"`
		Fields struct {
			Summary  string `json:"summary"`
			Created  string `json:"created"`
			Updated  string `json:"updated"`
			Priority *struct {
				Name string `json:"name"`
			} `json:"priority"`
			Status struct {
				Name string `json:"name"`
			} `json:"status"`
			Assignee *struct {
				DisplayName string `json:"displayName"`
			} `json:"assignee"`
			Reporter *struct {
				DisplayName string `json:"displayName"`
			} `json:"reporter"`
			Description any `json:"description"`
			Comment     *struct {
				Comments []struct {
					Author *struct {
						DisplayName string `json:"displayName"`
					} `json:"author"`
					Body    any    `json:"body"`
					Created string `json:"created"`
				} `json:"comments"`
			} `json:"comment"`
		} `json:"fields"`
	} `json:"issues"`
}

func FetchRecentTickets(cfg *Config, w Window) ([]Ticket, error) {
	if len(cfg.JiraProjects) == 0 {
		return nil, fmt.Errorf("no jira projects configured")
	}
	projects := strings.Join(quoted(cfg.JiraProjects), ",")

	var jql string
	if w.End.IsZero() {
		// Open-ended: last N minutes up to now. Round up to the minute.
		mins := int((w.Lookback + time.Minute - 1) / time.Minute)
		jql = fmt.Sprintf("project in (%s) AND updated >= -%dm ORDER BY updated DESC",
			projects, mins)
	} else {
		// Absolute window: [end-lookback, end]. JQL datetime literal format
		// is "yyyy-MM-dd HH:mm", interpreted in the Jira instance timezone.
		start := w.End.Add(-w.Lookback)
		const jqlFmt = "2006-01-02 15:04"
		jql = fmt.Sprintf(
			"project in (%s) AND updated >= \"%s\" AND updated <= \"%s\" ORDER BY updated DESC",
			projects, start.Format(jqlFmt), w.End.Format(jqlFmt),
		)
	}

	reqBody, _ := json.Marshal(map[string]any{
		"jql":        jql,
		"fields":     []string{"summary", "status", "priority", "assignee", "reporter", "created", "updated", "description", "comment"},
		"maxResults": 100,
	})

	endpoint := strings.TrimRight(cfg.JiraBaseURL, "/") + "/rest/api/3/search/jql"
	req, err := http.NewRequest("POST", endpoint, bytes.NewReader(reqBody))
	if err != nil {
		return nil, err
	}
	auth := base64.StdEncoding.EncodeToString([]byte(cfg.JiraEmail + ":" + cfg.JiraToken))
	req.Header.Set("Authorization", "Basic "+auth)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("jira request: %w", err)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 300 {
		return nil, fmt.Errorf("jira %d: %s", resp.StatusCode, string(body))
	}

	debugRaw("jira JQL", jql)
	debugRaw("jira raw response", string(body))

	var sr jiraSearchResp
	if err := json.Unmarshal(body, &sr); err != nil {
		return nil, fmt.Errorf("parse jira resp: %w", err)
	}

	base := strings.TrimRight(cfg.JiraBaseURL, "/")
	out := make([]Ticket, 0, len(sr.Issues))
	for _, iss := range sr.Issues {
		t := Ticket{
			Key:         iss.Key,
			Summary:     iss.Fields.Summary,
			Status:      iss.Fields.Status.Name,
			Description: flattenADF(iss.Fields.Description),
			URL:         base + "/browse/" + iss.Key,
		}
		if iss.Fields.Priority != nil {
			t.Priority = iss.Fields.Priority.Name
		}
		if iss.Fields.Assignee != nil {
			t.Assignee = iss.Fields.Assignee.DisplayName
		}
		if iss.Fields.Reporter != nil {
			t.Reporter = iss.Fields.Reporter.DisplayName
		}
		if ts, err := time.Parse("2006-01-02T15:04:05.000-0700", iss.Fields.Created); err == nil {
			t.Created = ts
		}
		if ts, err := time.Parse("2006-01-02T15:04:05.000-0700", iss.Fields.Updated); err == nil {
			t.Updated = ts
		}
		if iss.Fields.Comment != nil {
			for _, c := range iss.Fields.Comment.Comments {
				cm := Comment{
					Created: c.Created,
					Body:    flattenADFLimit(c.Body, 2000),
				}
				if c.Author != nil {
					cm.Author = c.Author.DisplayName
				}
				t.Comments = append(t.Comments, cm)
			}
		}
		out = append(out, t)
	}
	debugDump("parsed tickets (what we keep from Jira)", out)
	return out, nil
}

func quoted(xs []string) []string {
	out := make([]string, len(xs))
	for i, x := range xs {
		out[i] = "\"" + x + "\""
	}
	return out
}

// flattenADF best-effort extracts plain text from Jira's Atlassian Document Format.
func flattenADF(v any) string {
	return flattenADFLimit(v, 2000)
}

func flattenADFLimit(v any, limit int) string {
	if v == nil {
		return ""
	}
	var sb strings.Builder
	var walk func(node any)
	walk = func(node any) {
		m, ok := node.(map[string]any)
		if !ok {
			return
		}
		if t, _ := m["type"].(string); t == "text" {
			if s, ok := m["text"].(string); ok {
				sb.WriteString(s)
				sb.WriteString(" ")
			}
		}
		if content, ok := m["content"].([]any); ok {
			for _, c := range content {
				walk(c)
			}
		}
	}
	walk(v)
	s := strings.TrimSpace(sb.String())
	if limit > 0 && len(s) > limit {
		s = s[:limit] + "..."
	}
	return s
}
