package cloudconvert

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"time"
)

const BaseURL = "https://api.cloudconvert.com/v2"

type Client struct {
	httpClient  *http.Client
	apiKey      string
}

func NewClient(apiKey string) *Client {
	return &Client{
		httpClient: &http.Client{
			Timeout: 60 * time.Second,
		},
		apiKey: apiKey,
	}
}

type JobRequest struct {
	Tasks map[string]interface{} `json:"tasks"`
}

type JobResponse struct {
	Data Job `json:"data"`
}

type Job struct {
	ID     string `json:"id"`
	Status string `json:"status"`
	Tasks  []Task `json:"tasks"`
}

type Task struct {
	ID        string      `json:"id"`
	Name      string      `json:"name"`
	Operation string      `json:"operation"`
	Status    string      `json:"status"`
	Message   string      `json:"message"`
	Result    *TaskResult `json:"result"`
}

type TaskResult struct {
	Form  *UploadForm  `json:"form,omitempty"`
	Files []ExportFile `json:"files,omitempty"`
}

type UploadForm struct {
	URL        string            `json:"url"`
	Parameters map[string]interface{} `json:"parameters"`
}

type ExportFile struct {
	Filename string `json:"filename"`
	URL      string `json:"url"`
	Size     int64  `json:"size"`
}

func (c *Client) doRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
	var reqBody io.Reader
	if body != nil {
		jsonBytes, err := json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal request body: %w", err)
		}
		reqBody = bytes.NewReader(jsonBytes)
	}

	req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.apiKey)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	return c.httpClient.Do(req)
}

func (c *Client) CreateJob(ctx context.Context, jobReq *JobRequest) (*Job, error) {
	resp, err := c.doRequest(ctx, http.MethodPost, BaseURL+"/jobs", jobReq)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("failed to create job: status %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var jobResp JobResponse
	if err := json.NewDecoder(resp.Body).Decode(&jobResp); err != nil {
		return nil, fmt.Errorf("failed to decode job response: %w", err)
	}

	return &jobResp.Data, nil
}

func (c *Client) GetJob(ctx context.Context, jobID string) (*Job, error) {
	resp, err := c.doRequest(ctx, http.MethodGet, BaseURL+"/jobs/"+jobID, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("failed to get job: status %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var jobResp JobResponse
	if err := json.NewDecoder(resp.Body).Decode(&jobResp); err != nil {
		return nil, fmt.Errorf("failed to decode job response: %w", err)
	}

	return &jobResp.Data, nil
}

func (c *Client) UploadFile(ctx context.Context, form *UploadForm, filename string, fileData io.Reader) error {
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	for key, val := range form.Parameters {
		var strVal string
		switch v := val.(type) {
		case string:
			strVal = v
		case float64:
			strVal = fmt.Sprintf("%.0f", v)
		default:
			strVal = fmt.Sprintf("%v", v)
		}
		if err := writer.WriteField(key, strVal); err != nil {
			return fmt.Errorf("failed to write form field %s: %w", key, err)
		}
	}

	part, err := writer.CreateFormFile("file", filename)
	if err != nil {
		return fmt.Errorf("failed to create form file: %w", err)
	}

	if _, err := io.Copy(part, fileData); err != nil {
		return fmt.Errorf("failed to copy file data: %w", err)
	}

	if err := writer.Close(); err != nil {
		return fmt.Errorf("failed to close multipart writer: %w", err)
	}

	uploadClient := &http.Client{
		Timeout: 30 * time.Minute,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return nil
		},
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, form.URL, body)
	if err != nil {
		return fmt.Errorf("failed to create upload request: %w", err)
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())

	resp, err := uploadClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to upload file: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("upload failed: status %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	return nil
}

func (c *Client) WaitForJob(ctx context.Context, jobID string, pollInterval time.Duration, maxWait time.Duration) (*Job, error) {
	deadline := time.Now().Add(maxWait)

	for time.Now().Before(deadline) {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}

		job, err := c.GetJob(ctx, jobID)
		if err != nil {
			return nil, err
		}

		switch job.Status {
		case "finished":
			return job, nil
		case "error":
			for _, task := range job.Tasks {
				if task.Status == "error" {
					return nil, fmt.Errorf("job failed: task '%s' error: %s", task.Name, task.Message)
				}
			}
			return nil, fmt.Errorf("job failed with unknown error")
		}

		time.Sleep(pollInterval)
	}

	return nil, fmt.Errorf("job timed out after %v", maxWait)
}

func (c *Client) DownloadFile(ctx context.Context, url string) (io.ReadCloser, int64, error) {
	downloadClient := &http.Client{
		Timeout: 30 * time.Minute,
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to create download request: %w", err)
	}

	resp, err := downloadClient.Do(req)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to download file: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, 0, fmt.Errorf("download failed: status %d", resp.StatusCode)
	}

	return resp.Body, resp.ContentLength, nil
}

func (c *Client) CreateVideoCompressionJob(ctx context.Context) (*Job, error) {
	jobReq := &JobRequest{
		Tasks: map[string]interface{}{
			"import-video": map[string]interface{}{
				"operation": "import/upload",
			},
			"convert-video": map[string]interface{}{
				"operation":     "convert",
				"input":         "import-video",
				"output_format": "mp4",
				"video_codec":   "x264",
				"crf":           28,
				"preset":        "medium",
				"height":        720,
				"fit":           "max",
				"audio_codec":   "aac",
				"audio_bitrate": 128,
			},
			"export-video": map[string]interface{}{
				"operation": "export/url",
				"input":     "convert-video",
			},
		},
	}

	return c.CreateJob(ctx, jobReq)
}
