// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

package license

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

	"github.com/dagucloud/dagu/internal/cmn/config"
)

const defaultCloudURL = "https://console.dagu.sh"

// maxResponseSize is the maximum number of bytes read from a Cloud API response (1 MB).
const maxResponseSize = 1 << 20

// CloudClient communicates with the Dagu Cloud API for license operations.
type CloudClient struct {
	baseURL string
	client  *http.Client
}

// NewCloudClient creates a client for the given Cloud API base URL.
// If baseURL is empty, the production URL is used.
func NewCloudClient(baseURL string) *CloudClient {
	if baseURL == "" {
		baseURL = defaultCloudURL
	}
	return &CloudClient{
		baseURL: baseURL,
		client: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// ActivateRequest is the request body for license activation.
type ActivateRequest struct {
	Key           string `json:"key"`
	ServerID      string `json:"server_id"`
	MachineName   string `json:"machine_name"`
	ClientVersion string `json:"client_version,omitempty"`
}

// ActivateResponse is the response body from license activation.
type ActivateResponse struct {
	Token           string `json:"token"`
	HeartbeatSecret string `json:"heartbeat_secret"`
}

// HeartbeatRequest is the request body for heartbeat.
type HeartbeatRequest struct {
	LicenseID       string `json:"license_id"`
	ServerID        string `json:"server_id"`
	HeartbeatSecret string `json:"heartbeat_secret"`
	ClientVersion   string `json:"client_version,omitempty"`
}

// HeartbeatResponse is the response body from heartbeat.
type HeartbeatResponse struct {
	Token string `json:"token"`
}

// CloudError represents an error response from the Cloud API.
type CloudError struct {
	StatusCode int
	Message    string
}

type GitHubDispatchJob struct {
	ID                string          `json:"id"`
	BindingID         string          `json:"binding_id"`
	InstallationID    int64           `json:"installation_id"`
	RepositoryID      int64           `json:"repository_id"`
	RepositoryName    string          `json:"repository_name"`
	LicenseID         string          `json:"license_id"`
	DAGName           string          `json:"dag_name"`
	EventName         string          `json:"event_name"`
	EventAction       string          `json:"event_action"`
	DeliveryID        string          `json:"delivery_id"`
	Ref               string          `json:"ref"`
	SHA               string          `json:"sha"`
	PullRequestNumber int64           `json:"pull_request_number"`
	IssueNumber       int64           `json:"issue_number"`
	CommentID         int64           `json:"comment_id"`
	Command           string          `json:"command"`
	ActorLogin        string          `json:"actor_login"`
	CheckRunID        int64           `json:"check_run_id"`
	Status            string          `json:"status"`
	ClaimedBy         string          `json:"claimed_by"`
	DAGRunID          string          `json:"dag_run_id"`
	Payload           json.RawMessage `json:"payload"`
	Headers           json.RawMessage `json:"headers"`
}

type PullGitHubDispatchRequest struct {
	LicenseID       string `json:"license_id"`
	ServerID        string `json:"server_id"`
	HeartbeatSecret string `json:"heartbeat_secret"`
}

type AcceptGitHubDispatchRequest struct {
	LicenseID string `json:"license_id"`
	ServerID  string `json:"server_id"`
	Secret    string `json:"secret"`
	DAGRunID  string `json:"dag_run_id"`
}

type FinishGitHubDispatchRequest struct {
	LicenseID     string `json:"license_id"`
	ServerID      string `json:"server_id"`
	Secret        string `json:"secret"`
	ResultStatus  string `json:"result_status"`
	ResultSummary string `json:"result_summary,omitempty"`
}

func (e *CloudError) Error() string {
	return fmt.Sprintf("cloud API error (status %d): %s", e.StatusCode, e.Message)
}

// Activate exchanges a license key for a signed JWT token.
func (c *CloudClient) Activate(ctx context.Context, req ActivateRequest) (*ActivateResponse, error) {
	var resp ActivateResponse
	if err := c.doJSON(ctx, http.MethodPost, "/api/v1/licenses/activate", req, &resp); err != nil {
		return nil, err
	}
	return &resp, nil
}

// Heartbeat sends a heartbeat to keep the license active and get a refreshed token.
func (c *CloudClient) Heartbeat(ctx context.Context, req HeartbeatRequest) (*HeartbeatResponse, error) {
	var resp HeartbeatResponse
	if err := c.doJSON(ctx, http.MethodPost, "/api/v1/licenses/heartbeat", req, &resp); err != nil {
		return nil, err
	}
	return &resp, nil
}

func (c *CloudClient) PullGitHubDispatch(ctx context.Context, req PullGitHubDispatchRequest) (*GitHubDispatchJob, error) {
	var resp GitHubDispatchJob
	ok, err := c.doJSONOptional(ctx, http.MethodPost, "/api/v1/github/dispatch/pull", req, &resp)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, nil
	}
	if resp.ID == "" {
		return nil, fmt.Errorf("github dispatch response missing id")
	}
	return &resp, nil
}

func (c *CloudClient) AcceptGitHubDispatch(ctx context.Context, jobID string, req AcceptGitHubDispatchRequest) error {
	return c.doJSONAllowNoContent(ctx, http.MethodPost, "/api/v1/github/dispatch/"+url.PathEscape(jobID)+"/accept", req, nil)
}

func (c *CloudClient) FinishGitHubDispatch(ctx context.Context, jobID string, req FinishGitHubDispatchRequest) error {
	return c.doJSONAllowNoContent(ctx, http.MethodPost, "/api/v1/github/dispatch/"+url.PathEscape(jobID)+"/finish", req, nil)
}

func (c *CloudClient) doJSON(ctx context.Context, method, path string, reqBody, respBody any) error {
	body, err := json.Marshal(reqBody)
	if err != nil {
		return fmt.Errorf("failed to marshal request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "dagu-oss/"+config.Version)

	resp, err := c.client.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	respData, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
	if err != nil {
		return fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		msg := string(respData)
		// Try to extract a message from JSON error response
		var errResp struct {
			Message string `json:"message"`
		}
		if json.Unmarshal(respData, &errResp) == nil && errResp.Message != "" {
			msg = errResp.Message
		}
		return &CloudError{
			StatusCode: resp.StatusCode,
			Message:    msg,
		}
	}

	if err := json.Unmarshal(respData, respBody); err != nil {
		return fmt.Errorf("failed to unmarshal response: %w", err)
	}

	return nil
}

func (c *CloudClient) doJSONAllowNoContent(ctx context.Context, method, path string, reqBody, respBody any) error {
	_, err := c.doJSONOptional(ctx, method, path, reqBody, respBody)
	return err
}

func (c *CloudClient) doJSONOptional(ctx context.Context, method, path string, reqBody, respBody any) (bool, error) {
	body, err := json.Marshal(reqBody)
	if err != nil {
		return false, fmt.Errorf("failed to marshal request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(body))
	if err != nil {
		return false, fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "dagu-oss/"+config.Version)

	resp, err := c.client.Do(req)
	if err != nil {
		return false, fmt.Errorf("request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	respData, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
	if err != nil {
		return false, fmt.Errorf("failed to read response: %w", err)
	}
	if resp.StatusCode == http.StatusNoContent {
		return false, nil
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		msg := string(respData)
		var errResp struct {
			Message string `json:"message"`
		}
		if json.Unmarshal(respData, &errResp) == nil && errResp.Message != "" {
			msg = errResp.Message
		}
		return false, &CloudError{StatusCode: resp.StatusCode, Message: msg}
	}
	if respBody == nil {
		return true, nil
	}
	if len(respData) == 0 {
		return false, fmt.Errorf("empty response body for %s", path)
	}
	if err := json.Unmarshal(respData, respBody); err != nil {
		return false, fmt.Errorf("failed to unmarshal response: %w", err)
	}
	return true, nil
}
