//go:generate ${GOBIN}/mockgen -destination client_mock.go -package=oncall ${GO_MODULE}/internal/integrations/oncall Client
package oncall

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/logger"
	"github.com/tidbcloud/aisupport/services/ticket-manager/config"

	"github.com/go-resty/resty/v2"
)

type Client interface {
	ListTeams(ctx context.Context) ([]Team, error)
	ListTeamMembers(ctx context.Context, team string) ([]string, error)
	ListSchedule(ctx context.Context, param ListScheduleParam) ([]Schedule, error)
	GetScheduleResponders(ctx context.Context, schedule string, at time.Time) ([]string, error)
	Page(ctx context.Context, request PageRequest) (*PageResult, error)
}

type Team struct {
	TeamID   uint64 `json:"team_id"`
	TeamKey  string `json:"team_key"`
	TeamName string `json:"team_name"`
}

type ListScheduleParam struct {
	TeamIDs   []uint64
	StartTime time.Time
	EndTime   time.Time
}

type Schedule struct {
	EngineerEmail string `json:"engineer_email"`
	StartTime     int64  `json:"start_time"`
	EndTime       int64  `json:"end_time"`
}

type PageRequest struct {
	UserEmail string `json:"user_email"`
	Reason    string `json:"reason"`
}

type PageResult struct {
	MessageID   string `json:"message_id,omitempty"`
	PhoneNumber string `json:"phone_number,omitempty"`
	CallMethod  string `json:"call_method,omitempty"`
	CallError   string `json:"call_error,omitempty"`
}

type httpClient struct {
	resty *resty.Client
}

func NewClient(conf config.OncallConfig) Client {
	addr := strings.TrimRight(conf.Addr, "/")
	if !strings.Contains(addr, "://") {
		addr = "http://" + addr
	}
	client := resty.New().
		SetBaseURL(addr).
		SetTimeout(conf.Timeout).
		SetHeader("Accept", "application/json").
		SetAuthToken(conf.APIKey)
	if conf.Debug {
		client.SetPreRequestHook(func(_ *resty.Client, request *http.Request) error {
			body, err := requestBody(request)
			if err != nil {
				return err
			}
			logger.FromContext(request.Context()).Info("oncall request",
				"method", request.Method,
				"url", request.URL.String(),
				"body", body,
			)
			return nil
		})
		client.OnAfterResponse(func(_ *resty.Client, response *resty.Response) error {
			log := logger.FromContext(response.Request.Context())
			if strings.HasSuffix(response.Request.URL, "/api/v1/page") {
				// Page responses contain a phone number. Never write it to Ticket Manager logs.
				log.Info("oncall page response", "status_code", response.StatusCode())
			} else {
				log.Info("oncall response", "status_code", response.StatusCode(), "body", response.String())
			}
			return nil
		})
	}
	return &httpClient{resty: client}
}

type listTeamsResponse struct {
	Teams []Team `json:"teams"`
}

type listTeamMembersResponse struct {
	Engineers []struct {
		EngineerEmail string `json:"engineer_email"`
	} `json:"engineers"`
}

type scheduleResponse struct {
	Rows []Schedule `json:"rows"`
}

func (c *httpClient) ListTeams(ctx context.Context) ([]Team, error) {
	var teams listTeamsResponse
	response, err := c.resty.R().SetContext(ctx).SetResult(&teams).Get("/api/v1/teams")
	if err := responseError(response, err); err != nil {
		return nil, fmt.Errorf("list oncall teams: %w", err)
	}
	return teams.Teams, nil
}

func (c *httpClient) ListTeamMembers(ctx context.Context, team string) ([]string, error) {
	var members listTeamMembersResponse
	response, err := c.resty.R().
		SetContext(ctx).
		SetResult(&members).
		Get("/api/v1/teams/" + url.PathEscape(team) + "/available-engineers")
	if err := responseError(response, err); err != nil {
		return nil, fmt.Errorf("list oncall team members: %w", err)
	}
	emails := make([]string, 0, len(members.Engineers))
	for _, member := range members.Engineers {
		emails = append(emails, strings.ToLower(member.EngineerEmail))
	}
	return emails, nil
}

func (c *httpClient) ListSchedule(ctx context.Context, param ListScheduleParam) ([]Schedule, error) {
	teamIDs := make([]string, 0, len(param.TeamIDs))
	for _, teamID := range param.TeamIDs {
		teamIDs = append(teamIDs, strconv.FormatUint(teamID, 10))
	}
	var schedule scheduleResponse
	response, err := c.resty.R().
		SetContext(ctx).
		SetQueryParams(map[string]string{
			"team_ids":   strings.Join(teamIDs, ","),
			"start_time": strconv.FormatInt(param.StartTime.Unix(), 10),
			"end_time":   strconv.FormatInt(param.EndTime.Unix(), 10),
		}).
		SetResult(&schedule).
		Get("/api/v1/schedule")
	if err := responseError(response, err); err != nil {
		return nil, fmt.Errorf("get oncall schedule: %w", err)
	}
	return schedule.Rows, nil
}

type currentOncallResponse struct {
	CurrentEngineerEmails []string `json:"current_engineer_emails"`
}

func (c *httpClient) GetScheduleResponders(ctx context.Context, schedule string, at time.Time) ([]string, error) {
	var current currentOncallResponse
	response, err := c.resty.R().
		SetContext(ctx).
		SetQueryParam("at_time", strconv.FormatInt(at.Unix(), 10)).
		SetResult(&current).
		Get("/api/v1/teams/" + url.PathEscape(schedule) + "/current-oncall")
	if err := responseError(response, err); err != nil {
		return nil, fmt.Errorf("get schedule responders: %w", err)
	}
	responders := make([]string, 0, len(current.CurrentEngineerEmails))
	for _, email := range current.CurrentEngineerEmails {
		responders = append(responders, strings.ToLower(email))
	}
	return responders, nil
}

func (c *httpClient) Page(ctx context.Context, request PageRequest) (*PageResult, error) {
	var result PageResult
	response, err := c.resty.R().
		SetContext(ctx).
		SetBody(request).
		SetResult(&result).
		Post("/api/v1/page")
	if err := responseError(response, err); err != nil {
		return nil, fmt.Errorf("page oncall user: %w", err)
	}
	return &result, nil
}

func responseError(response *resty.Response, err error) error {
	if err != nil {
		return err
	}
	if response.IsError() {
		return fmt.Errorf("oncall returned HTTP %d", response.StatusCode())
	}
	return nil
}

func requestBody(request *http.Request) (string, error) {
	if request.GetBody == nil {
		return "", nil
	}
	body, err := request.GetBody()
	if err != nil {
		return "", err
	}
	defer body.Close()
	content, err := io.ReadAll(body)
	if err != nil {
		return "", err
	}
	return string(content), nil
}
