package activityhub_client

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/google/go-querystring/query"
)

const (
	errorStatus   = "error"
	notFoundError = "Activity not found."
)

type ActivityHubClient struct {
	client *http.Client
	addr   string
	source string
	key    string
}

type Activity struct {
	Type            string            `json:"type"`
	Name            string            `json:"name"`
	DisplayName     string            `json:"display_name"`
	Catalog         string            `json:"catalog"`
	Severity        int               `json:"severity"`
	ActivityID      string            `json:"activity_id"`
	ApplicationID   string            `json:"application_id"`
	ConceptID       string            `json:"concept_id"`
	TargetID        string            `json:"target_id"`
	CalibrationTime int64             `json:"calibration_time"`
	Source          string            `json:"source"`
	Author          string            `json:"author"`
	ContextID       string            `json:"context_id"`
	Context         []string          `json:"context"`
	Tags            map[string]string `json:"tags,omitempty"`
	Payload         any               `json:"payload"`
}

type Filter struct {
	ApplicationID string   `url:"application_id"`
	ConceptID     string   `url:"concept_id"`
	Type          []string `url:"type"`
	Name          []string `url:"name"`
	Catalog       []string `url:"catalog"`
	Author        string   `url:"author"`
	Source        []string `url:"source"`
	Limit         int      `url:"limit"`
	Desc          bool     `url:"desc"`
	Tags          []string `url:"tags"`
}

type activity struct {
	Type            string            `json:"type"`
	Name            string            `json:"name"`
	DisplayName     string            `json:"display_name"`
	Catalog         string            `json:"catalog"`
	Severity        json.Number       `json:"severity"`
	ActivityID      string            `json:"activity_id"`
	ApplicationID   string            `json:"application_id"`
	ConceptID       string            `json:"concept_id"`
	TargetID        string            `json:"target_id"`
	CalibrationTime json.Number       `json:"calibration_time"`
	Source          string            `json:"source"`
	Author          string            `json:"author"`
	ContextID       string            `json:"context_id"`
	Context         []string          `json:"context"`
	Tags            map[string]string `json:"tags"`
	Payload         any               `json:"payload"`
}

type getActivityResp struct {
	Result  *activity `json:"result"`
	Status  string    `json:"status"`
	Message string    `json:"msg"`
}

type getActivitiesResp struct {
	Result  []*activity `json:"result"`
	Partial bool        `json:"partial"`
	Cursor  string      `json:"cursor"`
	Status  string      `json:"status"`
	Message string      `json:"msg"`
}

type getContextIDResp struct {
	Result []string `json:"result"`
}

const requestLimit = 20

func NewClient(addr, source, key string) *ActivityHubClient {
	return &ActivityHubClient{
		client: &http.Client{},
		addr:   addr,
		source: source,
		key:    key,
	}
}

func (c *ActivityHubClient) addAuthHeader(req *http.Request) *http.Request {
	req.Header.Set("x-activityhub-source", c.source)
	req.Header.Set("x-activityhub-key", c.key)
	return req
}

func (a activity) toActivity() *Activity {
	severity, _ := a.Severity.Int64()
	calibrationTime, _ := a.CalibrationTime.Int64()

	return &Activity{
		Type:            a.Type,
		Name:            a.Name,
		DisplayName:     a.DisplayName,
		Catalog:         a.Catalog,
		Severity:        int(severity),
		ActivityID:      a.ActivityID,
		ApplicationID:   a.ApplicationID,
		ConceptID:       a.ConceptID,
		TargetID:        a.TargetID,
		CalibrationTime: calibrationTime,
		Source:          a.Source,
		Author:          a.Author,
		ContextID:       a.ContextID,
		Context:         a.Context,
		Tags:            a.Tags,
		Payload:         a.Payload,
	}
}

func toActivities(source []*activity) []*Activity {
	var result []*Activity
	for _, activity := range source {
		result = append(result, activity.toActivity())
	}
	return result
}

func (c *ActivityHubClient) GetActivity(id string) (*Activity, error) {
	return c.GetActivityWithContext(context.Background(), id)
}

func (c *ActivityHubClient) GetActivityWithContext(ctx context.Context, id string) (*Activity, error) {
	endpoint := fmt.Sprintf("%s/activity/%s", strings.TrimRight(c.addr, "/"), url.PathEscape(id))
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	req = c.addAuthHeader(req)
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var body getActivityResp
	d := json.NewDecoder(resp.Body)
	d.UseNumber()
	err = d.Decode(&body)
	if err != nil {
		return nil, err
	}

	if body.Status == errorStatus {
		if body.Message == notFoundError {
			return nil, nil
		}
		if body.Result == nil {
			return nil, errors.New(body.Message)
		}
		return body.Result.toActivity(), errors.New(body.Message)
	}
	if body.Result == nil {
		return nil, nil
	}

	return body.Result.toActivity(), nil
}

func (c *ActivityHubClient) ListActivitiesByTimeRange(targetID string, beginTS, endTS int64, cursor string, filter *Filter) ([]*Activity, bool, string, error) {
	q, err := query.Values(filter)
	if err != nil {
		return nil, false, "", err
	}
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/activities/%s/from/%d/to/%d?%s", c.addr, targetID, beginTS, endTS, q.Encode()), nil)
	if err != nil {
		return nil, false, "", err
	}
	req = c.addAuthHeader(req)
	if cursor != "" {
		req.Header.Set("x-activityhub-cursor", cursor)
	}

	resp, err := c.client.Do(req)
	if err != nil {
		return nil, false, "", err
	}
	defer resp.Body.Close()

	var body getActivitiesResp
	d := json.NewDecoder(resp.Body)
	d.UseNumber()
	err = d.Decode(&body)
	if err != nil {
		return nil, false, "", err
	}

	if body.Status == errorStatus && body.Message != notFoundError {
		return toActivities(body.Result), body.Partial, body.Cursor, errors.New(body.Message)
	}

	return toActivities(body.Result), body.Partial, body.Cursor, nil
}

func (c *ActivityHubClient) SearchActivitiesByTimeRange(targetID string, beginTS, endTS int64, limit int, cursor string, filter *Activity) ([]*Activity, bool, string, error) {
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/search/%s/from/%d/to/%d?limit=%d&severity=%d&catalog=%s&type=%s&name=%s&desc=true", c.addr, targetID, beginTS, endTS, limit, filter.Severity, filter.Catalog, filter.Type, filter.Name), nil)
	if err != nil {
		return nil, false, "", err
	}
	req = c.addAuthHeader(req)
	if cursor != "" {
		req.Header.Set("x-activityhub-cursor", cursor)
	}

	resp, err := c.client.Do(req)
	if err != nil {
		return nil, false, "", err
	}
	defer resp.Body.Close()

	var body getActivitiesResp
	d := json.NewDecoder(resp.Body)
	d.UseNumber()
	err = d.Decode(&body)
	if err != nil {
		return nil, false, "", err
	}

	if body.Status == errorStatus && body.Message != notFoundError {
		return toActivities(body.Result), body.Partial, body.Cursor, errors.New(body.Message)
	}

	return toActivities(body.Result), body.Partial, body.Cursor, nil
}

func (c *ActivityHubClient) GetActivitiesCountByTimeRange(targetID string, beginTS, endTS int64, severity int) (int, error) {
	activities, err := c.GetAllActivitiesByTimeRange(targetID, beginTS, endTS, severity)
	if err != nil {
		return 0, err
	}
	return len(activities), nil
}

func (c *ActivityHubClient) GetAllActivitiesByTimeRange(targetID string, beginTS, endTS int64, severity int) ([]*Activity, error) {
	var activities []*activity
	cursor := ""
	for i := 1; i <= requestLimit; i++ {
		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/search/%s/from/%d/to/%d?severity=%d", c.addr, targetID, beginTS, endTS, severity), nil)
		if err != nil {
			return nil, err
		}
		req = c.addAuthHeader(req)
		if cursor != "" {
			req.Header.Set("x-activityhub-cursor", cursor)
		}

		resp, err := c.client.Do(req)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()

		var body getActivitiesResp
		d := json.NewDecoder(resp.Body)
		d.UseNumber()
		err = d.Decode(&body)
		if err != nil {
			return nil, err
		}
		activities = append(activities, body.Result...)
		cursor = body.Cursor

		if body.Status == errorStatus && body.Message != notFoundError {
			return toActivities(body.Result), errors.New(body.Message)
		}

		if !body.Partial {
			break
		} else if i == requestLimit {
			return nil, fmt.Errorf("too many activities")
		}
	}

	return toActivities(activities), nil
}

func (c *ActivityHubClient) GetActivitiesByContextID(contextID, activityType string) ([]*Activity, error) {
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/activities/context/%s?type=%s", c.addr, contextID, activityType), nil)
	if err != nil {
		return nil, err
	}
	req = c.addAuthHeader(req)
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var body getActivitiesResp
	d := json.NewDecoder(resp.Body)
	d.UseNumber()
	err = d.Decode(&body)
	if err != nil {
		return nil, err
	}

	if body.Status == errorStatus && body.Message != notFoundError {
		return toActivities(body.Result), errors.New(body.Message)
	}

	return toActivities(body.Result), nil
}

func (c *ActivityHubClient) GetContextIDbyTimeRange(targetID string, beginTS, endTS int64) ([]string, error) {
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/context/%s/from/%d/to/%d", c.addr, targetID, beginTS, endTS), nil)
	if err != nil {
		return nil, err
	}
	req = c.addAuthHeader(req)
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var body getContextIDResp
	d := json.NewDecoder(resp.Body)
	err = d.Decode(&body)
	if err != nil {
		return nil, err
	}

	return body.Result, nil
}

func (c *ActivityHubClient) SendActivity(request Activity) error {
	if request.Type == "" {
		request.Type = "event"
	}

	data, err := json.Marshal(request)
	if err != nil {
		return err
	}
	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/activity", c.addr), bytes.NewBuffer(data))
	if err != nil {
		return err
	}

	// add auth header
	req = c.addAuthHeader(req)

	resp, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	if resp.StatusCode != http.StatusAccepted {
		body, err := io.ReadAll(resp.Body)
		return fmt.Errorf("send event failed, got status %s, error %s %v", resp.Status, string(body), err)
	}
	return nil
}
