package sync

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"strings"
	"time"
)

// azureConnector talks to Azure DevOps REST 7.1 with PAT basic auth.
type azureConnector struct {
	baseURL      string // https://dev.azure.com unless self-hosted
	organization string
	project      string
	pat          string
}

func (a *azureConnector) Authorize(req *http.Request) {
	req.SetBasicAuth("", a.pat)
}

func (a *azureConnector) orgURL() string {
	return strings.TrimRight(a.baseURL, "/") + "/" + a.organization
}

func (a *azureConnector) TestConnection(ctx context.Context) error {
	var out struct {
		Count int `json:"count"`
	}
	if err := getJSON(ctx, a.Authorize, a.orgURL()+"/_apis/projects?api-version=7.1", &out); err != nil {
		return fmt.Errorf("azure devops /_apis/projects: %w", err)
	}
	return nil
}

func (a *azureConnector) wiql(ctx context.Context, query string) ([]int, error) {
	body, _ := json.Marshal(map[string]string{"query": query})
	var out struct {
		WorkItems []struct {
			ID int `json:"id"`
		} `json:"workItems"`
	}
	u := fmt.Sprintf("%s/%s/_apis/wit/wiql?api-version=7.1", a.orgURL(), a.project)
	if err := doJSON(ctx, a.Authorize, http.MethodPost, u, body, &out); err != nil {
		return nil, fmt.Errorf("azure wiql: %w", err)
	}
	ids := make([]int, len(out.WorkItems))
	for i, wi := range out.WorkItems {
		ids[i] = wi.ID
	}
	return ids, nil
}

type azureWorkItem struct {
	ID     int            `json:"id"`
	Fields map[string]any `json:"fields"`
	Links  struct {
		HTML struct {
			Href string `json:"href"`
		} `json:"html"`
	} `json:"_links"`
	Relations []struct {
		Rel        string `json:"rel"`
		URL        string `json:"url"`
		Attributes struct {
			Name string `json:"name"`
		} `json:"attributes"`
	} `json:"relations"`
}

func (a *azureConnector) FetchUpdated(ctx context.Context, identity string, since time.Time) ([]Ticket, error) {
	q := fmt.Sprintf(`Select [System.Id] From WorkItems Where [System.AssignedTo] = '%s' AND [System.TeamProject] = '%s' AND [System.ChangedDate] >= '%s'`,
		identity, a.project, since.UTC().Format("2006-01-02T15:04:05Z"))
	ids, err := a.wiql(ctx, q)
	if err != nil || len(ids) == 0 {
		return nil, err
	}

	var out []Ticket
	for start := 0; start < len(ids); start += 200 {
		end := min(start+200, len(ids))
		idStrs := make([]string, 0, end-start)
		for _, id := range ids[start:end] {
			idStrs = append(idStrs, fmt.Sprint(id))
		}
		var batch struct {
			Value []azureWorkItem `json:"value"`
		}
		u := fmt.Sprintf("%s/%s/_apis/wit/workitems?ids=%s&$expand=all&api-version=7.1",
			a.orgURL(), a.project, strings.Join(idStrs, ","))
		if err := getJSON(ctx, a.Authorize, u, &batch); err != nil {
			return nil, fmt.Errorf("azure workitems batch: %w", err)
		}
		for _, wi := range batch.Value {
			out = append(out, a.toTicket(wi))
		}
	}
	return out, nil
}

func (a *azureConnector) toTicket(wi azureWorkItem) Ticket {
	title, _ := wi.Fields["System.Title"].(string)
	descHTML, _ := wi.Fields["System.Description"].(string)
	witype, _ := wi.Fields["System.WorkItemType"].(string)
	url := wi.Links.HTML.Href
	if url == "" {
		url = fmt.Sprintf("%s/%s/_workitems/edit/%d", a.orgURL(), a.project, wi.ID)
	}
	t := Ticket{
		Key:         fmt.Sprintf("%s-%d", strings.ToUpper(a.project), wi.ID),
		URL:         url,
		Type:        azureType(witype),
		Title:       title,
		Description: stripHTML(descHTML),
		Raw:         map[string]any{"id": wi.ID, "workItemType": witype},
	}
	for _, rel := range wi.Relations {
		if rel.Rel == "AttachedFile" {
			t.Attachments = append(t.Attachments, TicketAttachment{Name: rel.Attributes.Name, URL: rel.URL})
		}
	}
	return t
}

func (a *azureConnector) ListAssignedKeys(ctx context.Context, identity string) ([]string, error) {
	q := fmt.Sprintf(`Select [System.Id] From WorkItems Where [System.AssignedTo] = '%s' AND [System.TeamProject] = '%s'`,
		identity, a.project)
	ids, err := a.wiql(ctx, q)
	if err != nil {
		return nil, err
	}
	keys := make([]string, len(ids))
	for i, id := range ids {
		keys[i] = fmt.Sprintf("%s-%d", strings.ToUpper(a.project), id)
	}
	return keys, nil
}

func azureType(name string) string {
	switch strings.ToLower(name) {
	case "epic":
		return "epic"
	case "user story", "product backlog item":
		return "story"
	default:
		return "task"
	}
}

var tagRe = regexp.MustCompile(`<[^>]*>`)

// stripHTML is a lossy HTML→text flattener for ADO descriptions.
func stripHTML(s string) string {
	s = strings.ReplaceAll(s, "<br>", "\n")
	s = strings.ReplaceAll(s, "<br/>", "\n")
	s = strings.ReplaceAll(s, "</p>", "\n")
	s = tagRe.ReplaceAllString(s, "")
	s = strings.ReplaceAll(s, "&nbsp;", " ")
	s = strings.ReplaceAll(s, "&amp;", "&")
	s = strings.ReplaceAll(s, "&lt;", "<")
	s = strings.ReplaceAll(s, "&gt;", ">")
	return strings.TrimSpace(s)
}
