package sync

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

// wekanConnector imports cards from a WeKan board. projectKey is the board
// id; the consultant's identity (users.extra.ticketingIdentities.wekan) is
// their WeKan username — cards assigned to that user (assignees, falling
// back to members) become tasks.
//
// Credentials: {"username": "...", "password": "..."} — the connector logs
// in per sync (WeKan tokens are short-lived); alternatively a pre-issued
// {"token": "...", "userId": "..."} pair is used directly.
type wekanConnector struct {
	baseURL  string
	boardID  string
	username string
	password string

	mu      sync.Mutex
	token   string
	tokenAt time.Time
}

const wekanTokenTTL = 10 * time.Minute // re-login window for password auth

func (wk *wekanConnector) Authorize(req *http.Request) {
	wk.mu.Lock()
	defer wk.mu.Unlock()
	if wk.token != "" {
		req.Header.Set("Authorization", "Bearer "+wk.token)
	}
}

// ensureAuth logs in with username/password unless a still-fresh (or
// pre-issued) token is available.
func (wk *wekanConnector) ensureAuth(ctx context.Context) error {
	wk.mu.Lock()
	haveFresh := wk.token != "" && (wk.password == "" || time.Since(wk.tokenAt) < wekanTokenTTL)
	wk.mu.Unlock()
	if haveFresh {
		return nil
	}
	body, _ := json.Marshal(map[string]string{"username": wk.username, "password": wk.password})
	var out struct {
		ID    string `json:"id"`
		Token string `json:"token"`
	}
	err := doJSON(ctx, func(*http.Request) {}, http.MethodPost, wk.baseURL+"/users/login", body, &out)
	if err != nil {
		return fmt.Errorf("wekan login: %w", err)
	}
	if out.Token == "" {
		return fmt.Errorf("wekan login: empty token in response")
	}
	wk.mu.Lock()
	wk.token, wk.tokenAt = out.Token, time.Now()
	wk.mu.Unlock()
	return nil
}

func (wk *wekanConnector) TestConnection(ctx context.Context) error {
	if err := wk.ensureAuth(ctx); err != nil {
		return err
	}
	var board struct {
		Title string `json:"title"`
	}
	if err := getJSON(ctx, wk.Authorize, wk.baseURL+"/api/boards/"+wk.boardID, &board); err != nil {
		return fmt.Errorf("wekan board %s: %w", wk.boardID, err)
	}
	if board.Title == "" {
		return fmt.Errorf("wekan board %s: not found or no access", wk.boardID)
	}
	return nil
}

type wekanBoard struct {
	Title   string `json:"title"`
	Slug    string `json:"slug"`
	Members []struct {
		UserID string `json:"userId"`
	} `json:"members"`
}

type wekanCardDetail struct {
	ID               string   `json:"_id"`
	Title            string   `json:"title"`
	Description      string   `json:"description"`
	ListID           string   `json:"listId"`
	Assignees        []string `json:"assignees"`
	Members          []string `json:"members"`
	ModifiedAt       string   `json:"modifiedAt"`
	DateLastActivity string   `json:"dateLastActivity"`
	Archived         bool     `json:"archived"`
}

// resolveUserID maps the consultant's WeKan username to a board member's
// userId.
func (wk *wekanConnector) resolveUserID(ctx context.Context, board *wekanBoard, username string) (string, error) {
	for _, m := range board.Members {
		var u struct {
			Username string `json:"username"`
		}
		if err := getJSON(ctx, wk.Authorize, wk.baseURL+"/api/users/"+m.UserID, &u); err != nil {
			continue // non-admin tokens may not read every user; skip
		}
		if strings.EqualFold(u.Username, username) {
			return m.UserID, nil
		}
	}
	return "", fmt.Errorf("wekan user %q is not a member of board %s", username, wk.boardID)
}

// assignedCards walks lists → cards → card details and returns the cards
// assigned to userID (assignees, else members).
func (wk *wekanConnector) assignedCards(ctx context.Context, userID string) ([]wekanCardDetail, map[string]string, error) {
	var lists []struct {
		ID    string `json:"_id"`
		Title string `json:"title"`
	}
	if err := getJSON(ctx, wk.Authorize, wk.baseURL+"/api/boards/"+wk.boardID+"/lists", &lists); err != nil {
		return nil, nil, fmt.Errorf("wekan lists: %w", err)
	}
	listTitles := map[string]string{}
	var out []wekanCardDetail
	for _, list := range lists {
		listTitles[list.ID] = list.Title
		var cards []struct {
			ID string `json:"_id"`
		}
		url := fmt.Sprintf("%s/api/boards/%s/lists/%s/cards", wk.baseURL, wk.boardID, list.ID)
		if err := getJSON(ctx, wk.Authorize, url, &cards); err != nil {
			return nil, nil, fmt.Errorf("wekan cards of list %s: %w", list.ID, err)
		}
		for _, c := range cards {
			var detail wekanCardDetail
			detailURL := fmt.Sprintf("%s/api/boards/%s/lists/%s/cards/%s", wk.baseURL, wk.boardID, list.ID, c.ID)
			if err := getJSON(ctx, wk.Authorize, detailURL, &detail); err != nil {
				return nil, nil, fmt.Errorf("wekan card %s: %w", c.ID, err)
			}
			if detail.Archived {
				continue
			}
			assigned := detail.Assignees
			if len(assigned) == 0 {
				assigned = detail.Members
			}
			for _, a := range assigned {
				if a == userID {
					detail.ListID = list.ID
					out = append(out, detail)
					break
				}
			}
		}
	}
	return out, listTitles, nil
}

func (wk *wekanConnector) cardTime(c wekanCardDetail) time.Time {
	for _, raw := range []string{c.ModifiedAt, c.DateLastActivity} {
		if raw == "" {
			continue
		}
		if ts, err := time.Parse(time.RFC3339, raw); err == nil {
			return ts
		}
	}
	return time.Time{} // unknown → treat as always-updated
}

func (wk *wekanConnector) FetchUpdated(ctx context.Context, identity string, since time.Time) ([]Ticket, error) {
	if err := wk.ensureAuth(ctx); err != nil {
		return nil, err
	}
	var board wekanBoard
	if err := getJSON(ctx, wk.Authorize, wk.baseURL+"/api/boards/"+wk.boardID, &board); err != nil {
		return nil, fmt.Errorf("wekan board: %w", err)
	}
	userID, err := wk.resolveUserID(ctx, &board, identity)
	if err != nil {
		return nil, err
	}
	cards, listTitles, err := wk.assignedCards(ctx, userID)
	if err != nil {
		return nil, err
	}

	slug := board.Slug
	if slug == "" {
		slug = "board"
	}
	out := []Ticket{}
	for _, c := range cards {
		if ts := wk.cardTime(c); !ts.IsZero() && ts.Before(since) {
			continue
		}
		out = append(out, Ticket{
			Key:         c.ID,
			URL:         fmt.Sprintf("%s/b/%s/%s/%s", wk.baseURL, wk.boardID, slug, c.ID),
			Type:        "task", // kanban cards carry no epic/story hierarchy
			Title:       c.Title,
			Description: c.Description,
			Raw: map[string]any{
				"board": board.Title, "list": listTitles[c.ListID], "cardId": c.ID,
			},
		})
	}
	return out, nil
}

func (wk *wekanConnector) ListAssignedKeys(ctx context.Context, identity string) ([]string, error) {
	if err := wk.ensureAuth(ctx); err != nil {
		return nil, err
	}
	var board wekanBoard
	if err := getJSON(ctx, wk.Authorize, wk.baseURL+"/api/boards/"+wk.boardID, &board); err != nil {
		return nil, fmt.Errorf("wekan board: %w", err)
	}
	userID, err := wk.resolveUserID(ctx, &board, identity)
	if err != nil {
		return nil, err
	}
	cards, _, err := wk.assignedCards(ctx, userID)
	if err != nil {
		return nil, err
	}
	keys := make([]string, len(cards))
	for i, c := range cards {
		keys[i] = c.ID
	}
	return keys, nil
}
