package app

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"sync"
	"time"
)

var ErrFeedbackReminderRunning = errors.New("feedback reminder is already running")

type FeedbackReminderTarget struct {
	IssueKey     string
	RunCreatedAt time.Time
}

type FeedbackReminderAssignee struct {
	DisplayName  string
	EmailAddress string
}

type FeedbackReminderTicket struct {
	IssueKey string
	JiraURL  string
}

type FeedbackReminderGroup struct {
	DisplayName string
	OpenID      string
	Tickets     []FeedbackReminderTicket
}

type FeedbackReminderResult struct {
	TicketCount   int
	AssigneeCount int
	Sent          bool
}

type FeedbackReminderRunner interface {
	Run(context.Context, string) (FeedbackReminderResult, error)
}

type FeedbackReminderSource interface {
	ListTargets(context.Context, time.Time) ([]FeedbackReminderTarget, error)
}

type FeedbackReminderJira interface {
	CurrentAssignee(context.Context, string) (FeedbackReminderAssignee, error)
}

type FeedbackReminderDirectory interface {
	ResolveByEmails(context.Context, []string) (map[string]string, error)
	ResolveByExactNames(context.Context, []string) (map[string]string, error)
}

type FeedbackReminderSender interface {
	SendFeedbackReminder(context.Context, string, string, []FeedbackReminderGroup) error
}

type FeedbackReminder struct {
	source       FeedbackReminderSource
	jira         FeedbackReminderJira
	directory    FeedbackReminderDirectory
	sender       FeedbackReminderSender
	chatID       string
	jiraBaseURL  string
	lookbackDays int
	now          func() time.Time
	mu           sync.Mutex
}

func NewFeedbackReminder(config Config, messenger *LarkMessenger) (*FeedbackReminder, error) {
	client := &http.Client{Timeout: 30 * time.Second}
	return &FeedbackReminder{
		source:       &HTTPFeedbackReminderSource{baseURL: config.AnalysisBaseURL, client: client},
		jira:         &HTTPFeedbackReminderJira{baseURL: config.JiraBaseURL, username: config.JiraUsername, apiToken: config.JiraAPIToken, client: client},
		directory:    &CoreLarkFeedbackReminderDirectory{coreUserBaseURL: config.FeedbackReminderCoreUserURL, client: client},
		sender:       messenger,
		chatID:       config.FeedbackReminderChatID,
		jiraBaseURL:  config.JiraBaseURL,
		lookbackDays: config.FeedbackReminderLookbackDays,
		now:          time.Now,
	}, nil
}

func (r *FeedbackReminder) Run(ctx context.Context, idempotencyKey string) (FeedbackReminderResult, error) {
	if !r.mu.TryLock() {
		return FeedbackReminderResult{}, ErrFeedbackReminderRunning
	}
	defer r.mu.Unlock()
	targets, err := r.source.ListTargets(ctx, r.now().Add(-time.Duration(r.lookbackDays)*24*time.Hour))
	if err != nil {
		return FeedbackReminderResult{}, fmt.Errorf("list feedback reminder targets: %w", err)
	}
	if len(targets) == 0 {
		return FeedbackReminderResult{}, nil
	}
	assignees := make([]FeedbackReminderAssignee, 0, len(targets))
	for _, target := range targets {
		assignee, err := r.jira.CurrentAssignee(ctx, target.IssueKey)
		if err != nil {
			return FeedbackReminderResult{}, fmt.Errorf("load %s assignee: %w", target.IssueKey, err)
		}
		if strings.TrimSpace(assignee.DisplayName) == "" {
			return FeedbackReminderResult{}, fmt.Errorf("%s has no current Jira assignee", target.IssueKey)
		}
		assignees = append(assignees, assignee)
	}
	openIDs, err := r.resolveOpenIDs(ctx, assignees)
	if err != nil {
		return FeedbackReminderResult{}, err
	}
	groups := make([]FeedbackReminderGroup, 0)
	groupIndexes := map[string]int{}
	for index, target := range targets {
		assignee := assignees[index]
		key := strings.ToLower(strings.TrimSpace(assignee.EmailAddress))
		if key == "" {
			key = "name:" + strings.ToLower(strings.TrimSpace(assignee.DisplayName))
		}
		openID := openIDs[key]
		if openID == "" {
			return FeedbackReminderResult{}, fmt.Errorf("cannot resolve Feishu mention for %s", assignee.DisplayName)
		}
		groupIndex, ok := groupIndexes[key]
		if !ok {
			groupIndex = len(groups)
			groupIndexes[key] = groupIndex
			groups = append(groups, FeedbackReminderGroup{DisplayName: assignee.DisplayName, OpenID: openID})
		}
		groups[groupIndex].Tickets = append(groups[groupIndex].Tickets, FeedbackReminderTicket{
			IssueKey: target.IssueKey,
			JiraURL:  r.jiraBaseURL + "/browse/" + url.PathEscape(target.IssueKey),
		})
	}
	if err := r.sender.SendFeedbackReminder(ctx, r.chatID, boundedReminderUUID(idempotencyKey), groups); err != nil {
		return FeedbackReminderResult{}, err
	}
	return FeedbackReminderResult{TicketCount: len(targets), AssigneeCount: len(groups), Sent: true}, nil
}

func (r *FeedbackReminder) resolveOpenIDs(ctx context.Context, assignees []FeedbackReminderAssignee) (map[string]string, error) {
	emails := make([]string, 0, len(assignees))
	for _, assignee := range assignees {
		if email := strings.ToLower(strings.TrimSpace(assignee.EmailAddress)); email != "" {
			emails = append(emails, email)
		}
	}
	byEmail, err := r.directory.ResolveByEmails(ctx, uniqueStrings(emails))
	if err != nil {
		return nil, fmt.Errorf("resolve Feishu users by email: %w", err)
	}
	names := make([]string, 0)
	for _, assignee := range assignees {
		email := strings.ToLower(strings.TrimSpace(assignee.EmailAddress))
		if email == "" || byEmail[email] == "" {
			names = append(names, strings.TrimSpace(assignee.DisplayName))
		}
	}
	byName, err := r.directory.ResolveByExactNames(ctx, uniqueStrings(names))
	if err != nil {
		return nil, fmt.Errorf("resolve Feishu users by name: %w", err)
	}
	result := map[string]string{}
	for _, assignee := range assignees {
		email := strings.ToLower(strings.TrimSpace(assignee.EmailAddress))
		nameKey := strings.ToLower(strings.TrimSpace(assignee.DisplayName))
		if email != "" && byEmail[email] != "" {
			result[email] = byEmail[email]
			continue
		}
		if byName[nameKey] != "" {
			key := email
			if key == "" {
				key = "name:" + nameKey
			}
			result[key] = byName[nameKey]
		}
	}
	return result, nil
}

type HTTPFeedbackReminderSource struct {
	baseURL string
	client  *http.Client
}

func (s *HTTPFeedbackReminderSource) ListTargets(ctx context.Context, since time.Time) ([]FeedbackReminderTarget, error) {
	query := url.Values{}
	query.Set("since", since.UTC().Format(time.RFC3339Nano))
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.baseURL+"/v1/auto-analysis/feedback-reminder-targets?"+query.Encode(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := s.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("feedback reminder target API status %d", resp.StatusCode)
	}
	var payload struct {
		Targets []FeedbackReminderTarget `json:"targets"`
	}
	if err := json.NewDecoder(io.LimitReader(resp.Body, 4*1024*1024)).Decode(&payload); err != nil {
		return nil, err
	}
	return payload.Targets, nil
}

type HTTPFeedbackReminderJira struct {
	baseURL  string
	username string
	apiToken string
	client   *http.Client
}

func (j *HTTPFeedbackReminderJira) CurrentAssignee(ctx context.Context, issueKey string) (FeedbackReminderAssignee, error) {
	requestURL := j.baseURL + "/rest/api/3/issue/" + url.PathEscape(issueKey) + "?fields=assignee"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
	if err != nil {
		return FeedbackReminderAssignee{}, err
	}
	req.SetBasicAuth(j.username, j.apiToken)
	req.Header.Set("Accept", "application/json")
	resp, err := j.client.Do(req)
	if err != nil {
		return FeedbackReminderAssignee{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return FeedbackReminderAssignee{}, fmt.Errorf("Jira status %d", resp.StatusCode)
	}
	var payload struct {
		Fields struct {
			Assignee *struct {
				DisplayName  string `json:"displayName"`
				EmailAddress string `json:"emailAddress"`
			} `json:"assignee"`
		} `json:"fields"`
	}
	if err := json.NewDecoder(io.LimitReader(resp.Body, 1024*1024)).Decode(&payload); err != nil {
		return FeedbackReminderAssignee{}, err
	}
	if payload.Fields.Assignee == nil {
		return FeedbackReminderAssignee{}, nil
	}
	return FeedbackReminderAssignee{
		DisplayName:  strings.TrimSpace(payload.Fields.Assignee.DisplayName),
		EmailAddress: strings.ToLower(strings.TrimSpace(payload.Fields.Assignee.EmailAddress)),
	}, nil
}

type CoreLarkFeedbackReminderDirectory struct {
	coreUserBaseURL string
	client          *http.Client
}

func (d *CoreLarkFeedbackReminderDirectory) ResolveByEmails(ctx context.Context, emails []string) (map[string]string, error) {
	result := map[string]string{}
	for _, email := range emails {
		normalized := strings.ToLower(strings.TrimSpace(email))
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.coreUserBaseURL+"/api/v1/core/user/users/"+url.PathEscape(normalized), nil)
		if err != nil {
			return nil, err
		}
		resp, err := d.client.Do(req)
		if err != nil {
			return nil, err
		}
		if resp.StatusCode == http.StatusNotFound {
			_ = resp.Body.Close()
			continue
		}
		var payload struct {
			User *struct {
				Email          string `json:"email"`
				LarkUserOpenID string `json:"larkUserOpenId"`
			} `json:"user"`
		}
		decodeErr := json.NewDecoder(io.LimitReader(resp.Body, 1024*1024)).Decode(&payload)
		_ = resp.Body.Close()
		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
			return nil, fmt.Errorf("Core User status %d", resp.StatusCode)
		}
		if decodeErr != nil {
			return nil, decodeErr
		}
		if payload.User != nil && larkOpenIDPattern.MatchString(strings.TrimSpace(payload.User.LarkUserOpenID)) {
			result[normalized] = strings.TrimSpace(payload.User.LarkUserOpenID)
		}
	}
	return result, nil
}

func (d *CoreLarkFeedbackReminderDirectory) ResolveByExactNames(ctx context.Context, names []string) (map[string]string, error) {
	result := map[string]string{}
	for _, name := range names {
		query := url.Values{}
		query.Set("userKeyword", name)
		query.Set("includeUnavailable", "true")
		query.Set("page", "1")
		query.Set("perPage", "20")
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.coreUserBaseURL+"/api/v1/core/user/users?"+query.Encode(), nil)
		if err != nil {
			return nil, err
		}
		resp, err := d.client.Do(req)
		if err != nil {
			return nil, err
		}
		var payload struct {
			Users []struct {
				LarkUserOpenID string `json:"larkUserOpenId"`
				LarkUserName   string `json:"larkUserName"`
				LarkUserEnName string `json:"larkUserEnName"`
			} `json:"users"`
		}
		decodeErr := json.NewDecoder(io.LimitReader(resp.Body, 1024*1024)).Decode(&payload)
		_ = resp.Body.Close()
		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
			return nil, fmt.Errorf("Core User status %d", resp.StatusCode)
		}
		if decodeErr != nil {
			return nil, decodeErr
		}
		matches := map[string]struct{}{}
		for _, user := range payload.Users {
			if strings.EqualFold(strings.TrimSpace(user.LarkUserName), name) || strings.EqualFold(strings.TrimSpace(user.LarkUserEnName), name) {
				if openID := strings.TrimSpace(user.LarkUserOpenID); openID != "" {
					matches[openID] = struct{}{}
				}
			}
		}
		if len(matches) == 1 {
			for openID := range matches {
				result[strings.ToLower(strings.TrimSpace(name))] = openID
			}
		}
	}
	return result, nil
}

type FeedbackReminderScheduler struct {
	runner   FeedbackReminderRunner
	location *time.Location
	hour     int
	now      func() time.Time
}

func NewFeedbackReminderScheduler(runner FeedbackReminderRunner, location *time.Location, hour int) *FeedbackReminderScheduler {
	return &FeedbackReminderScheduler{runner: runner, location: location, hour: hour, now: time.Now}
}

func (s *FeedbackReminderScheduler) Start(ctx context.Context) {
	go func() {
		for {
			next := nextFeedbackReminderRun(s.now(), s.location, s.hour)
			timer := time.NewTimer(time.Until(next))
			select {
			case <-ctx.Done():
				if !timer.Stop() {
					<-timer.C
				}
				return
			case <-timer.C:
				day := next.In(s.location).Format("2006-01-02")
				result, err := s.runner.Run(ctx, "fb-reminder-scheduled:"+day)
				if err != nil && ctx.Err() == nil {
					slog.Error("send scheduled feedback reminder", "error", err)
				} else {
					slog.Info("scheduled feedback reminder completed", "tickets", result.TicketCount, "assignees", result.AssigneeCount, "sent", result.Sent)
				}
			}
		}
	}()
}

func nextFeedbackReminderRun(now time.Time, location *time.Location, hour int) time.Time {
	localNow := now.In(location)
	for dayOffset := 0; ; dayOffset++ {
		day := localNow.AddDate(0, 0, dayOffset)
		candidate := time.Date(day.Year(), day.Month(), day.Day(), hour, 0, 0, 0, location)
		if candidate.After(localNow) && candidate.Weekday() != time.Saturday && candidate.Weekday() != time.Sunday {
			return candidate
		}
	}
}

func boundedReminderUUID(value string) string {
	value = strings.TrimSpace(value)
	if len(value) <= 50 {
		return value
	}
	return value[:50]
}

func uniqueStrings(values []string) []string {
	seen := map[string]struct{}{}
	result := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		key := strings.ToLower(value)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		result = append(result, value)
	}
	sort.Strings(result)
	return result
}
