// Package sync imports tickets from customer ticketing systems by polling
// (§5.3) and exposes per-system connectors used for both syncing and the
// admin "Test connection" button.
package sync

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"bountyboard/internal/domain"
)

// Ticket is the system-agnostic shape a connector returns.
type Ticket struct {
	Key         string
	URL         string
	Type        string // epic | story | task
	Title       string
	Description string
	Attachments []TicketAttachment
	Raw         map[string]any
}

type TicketAttachment struct {
	Name     string
	URL      string
	MimeType string
}

// Connector talks to one customer's ticketing system.
type Connector interface {
	// TestConnection performs a cheap authenticated call (§5.3).
	TestConnection(ctx context.Context) error
	// FetchUpdated returns tickets assigned to identity updated since the
	// watermark (callers pass lastSyncAt − 5 min for the overlap window).
	FetchUpdated(ctx context.Context, identity string, since time.Time) ([]Ticket, error)
	// ListAssignedKeys returns every ticket key currently assigned to
	// identity — used to flag orphaned tasks.
	ListAssignedKeys(ctx context.Context, identity string) ([]string, error)
	// Authorize adds the system's auth headers to an attachment download.
	Authorize(req *http.Request)
}

// Credentials is the decrypted per-system credential map (§4.2 shapes).
type Credentials map[string]string

// NewConnector builds the connector for a ticketing config + decrypted
// credentials.
func NewConnector(t domain.TicketingType, baseURL, projectKey string, creds Credentials) (Connector, error) {
	switch t {
	case domain.TicketingJira:
		if creds["email"] == "" || creds["apiToken"] == "" {
			return nil, fmt.Errorf("jira requires email and apiToken")
		}
		return &jiraConnector{baseURL: baseURL, projectKey: projectKey, email: creds["email"], apiToken: creds["apiToken"]}, nil
	case domain.TicketingAzure:
		if creds["organization"] == "" || creds["project"] == "" || creds["pat"] == "" {
			return nil, fmt.Errorf("azure_devops requires organization, project and pat")
		}
		if baseURL == "" {
			baseURL = "https://dev.azure.com"
		}
		return &azureConnector{baseURL: baseURL, organization: creds["organization"], project: creds["project"], pat: creds["pat"]}, nil
	case domain.TicketingYouTrack:
		if creds["permanentToken"] == "" {
			return nil, fmt.Errorf("youtrack requires permanentToken")
		}
		return &youtrackConnector{baseURL: baseURL, projectKey: projectKey, token: creds["permanentToken"]}, nil
	case domain.TicketingWekan:
		hasLogin := creds["username"] != "" && creds["password"] != ""
		hasToken := creds["token"] != ""
		if !hasLogin && !hasToken {
			return nil, fmt.Errorf("wekan requires username+password (or a pre-issued token)")
		}
		if baseURL == "" {
			return nil, fmt.Errorf("wekan requires baseUrl")
		}
		if projectKey == "" {
			return nil, fmt.Errorf("wekan requires projectKey (the board id)")
		}
		return &wekanConnector{
			baseURL: baseURL, boardID: projectKey,
			username: creds["username"], password: creds["password"],
			token: creds["token"],
		}, nil
	case domain.TicketingDemo:
		return &demoConnector{projectKey: projectKey}, nil
	default:
		return nil, fmt.Errorf("unknown ticketing type %q", t)
	}
}

var httpClient = &http.Client{Timeout: 30 * time.Second}

// getJSON performs an authorized GET and decodes JSON, with one retry on
// 5xx/network errors (§12).
func getJSON(ctx context.Context, authorize func(*http.Request), url string, dst any) error {
	return doJSON(ctx, authorize, http.MethodGet, url, nil, dst)
}

func doJSON(ctx context.Context, authorize func(*http.Request), method, url string, body []byte, dst any) error {
	var lastErr error
	for attempt := 0; attempt < 2; attempt++ {
		req, err := http.NewRequestWithContext(ctx, method, url, bodyReader(body))
		if err != nil {
			return err
		}
		if body != nil {
			req.Header.Set("Content-Type", "application/json")
		}
		req.Header.Set("Accept", "application/json")
		authorize(req)
		resp, err := httpClient.Do(req)
		if err != nil {
			lastErr = err
			continue
		}
		data, err := io.ReadAll(io.LimitReader(resp.Body, 20<<20))
		resp.Body.Close()
		if err != nil {
			lastErr = err
			continue
		}
		if resp.StatusCode >= 500 {
			lastErr = fmt.Errorf("%s %s: status %d", method, url, resp.StatusCode)
			continue
		}
		if resp.StatusCode >= 400 {
			return fmt.Errorf("%s %s: status %d: %.200s", method, url, resp.StatusCode, data)
		}
		if dst == nil {
			return nil
		}
		if err := json.Unmarshal(data, dst); err != nil {
			return fmt.Errorf("decode %s: %w", url, err)
		}
		return nil
	}
	return lastErr
}

func bodyReader(b []byte) io.Reader {
	if b == nil {
		return nil
	}
	return bytes.NewReader(b)
}
