package vimeo

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

const (
	BaseURL    = "https://api.vimeo.com"
	APIVersion = "application/vnd.vimeo.*+json;version=3.4"
)

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

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

type Video struct {
	URI            string     `json:"uri"`
	Name           string     `json:"name"`
	Description    string     `json:"description"`
	Duration       int        `json:"duration"`
	Width          int        `json:"width"`
	Height         int        `json:"height"`
	Link           string     `json:"link"`
	PlayerEmbedURL string     `json:"player_embed_url"`
	Pictures       *Pictures  `json:"pictures"`
	Status         string     `json:"status"`
	Transcode      *Transcode `json:"transcode"`
	Privacy        *Privacy   `json:"privacy"`
	Embed          *Embed     `json:"embed"`
	CreatedTime    time.Time  `json:"created_time"`
	ModifiedTime   time.Time  `json:"modified_time"`
}

type Pictures struct {
	URI     string `json:"uri"`
	Active  bool   `json:"active"`
	Sizes   []Size `json:"sizes"`
	BaseURL string `json:"base_link"`
}

type Size struct {
	Width  int    `json:"width"`
	Height int    `json:"height"`
	Link   string `json:"link"`
}

type Transcode struct {
	Status string `json:"status"`
}

type Privacy struct {
	View     string `json:"view"`
	Embed    string `json:"embed"`
	Download bool   `json:"download"`
}

type Embed struct {
	HTML   string `json:"html"`
	Badges struct {
		HDR              bool                     `json:"hdr"`
		Live             struct{ Streaming bool } `json:"live"`
		StaffPick        struct{ Normal bool }    `json:"staff_pick"`
		VOD              bool                     `json:"vod"`
		WeekendChallenge bool                     `json:"weekend_challenge"`
	} `json:"badges"`
}

type UploadRequest struct {
	Name        string         `json:"name,omitempty"`
	Description string         `json:"description,omitempty"`
	Upload      UploadParams   `json:"upload"`
	Privacy     *PrivacyParams `json:"privacy,omitempty"`
}

type UploadParams struct {
	Approach    string `json:"approach"`
	Size        int64  `json:"size,omitempty"`
	Link        string `json:"link,omitempty"`
	RedirectURL string `json:"redirect_url,omitempty"`
}

type PrivacyParams struct {
	View     string `json:"view,omitempty"`
	Embed    string `json:"embed,omitempty"`
	Download bool   `json:"download,omitempty"`
}

type UploadResponse struct {
	URI    string `json:"uri"`
	Name   string `json:"name"`
	Link   string `json:"link"`
	Upload struct {
		Status     string `json:"status"`
		UploadLink string `json:"upload_link"`
		Approach   string `json:"approach"`
		Size       int64  `json:"size"`
	} `json:"upload"`
	Transcode *Transcode `json:"transcode"`
}

type OEmbedResponse struct {
	Type            string `json:"type"`
	Version         string `json:"version"`
	ProviderName    string `json:"provider_name"`
	ProviderURL     string `json:"provider_url"`
	Title           string `json:"title"`
	AuthorName      string `json:"author_name"`
	AuthorURL       string `json:"author_url"`
	IsPlus          string `json:"is_plus"`
	HTML            string `json:"html"`
	Width           int    `json:"width"`
	Height          int    `json:"height"`
	Duration        int    `json:"duration"`
	Description     string `json:"description"`
	ThumbnailURL    string `json:"thumbnail_url"`
	ThumbnailWidth  int    `json:"thumbnail_width"`
	ThumbnailHeight int    `json:"thumbnail_height"`
	VideoID         int64  `json:"video_id"`
}

type UpdateVideoRequest struct {
	Name        *string        `json:"name,omitempty"`
	Description *string        `json:"description,omitempty"`
	Privacy     *PrivacyParams `json:"privacy,omitempty"`
}

func (c *Client) doRequest(ctx context.Context, method, path 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, BaseURL+path, reqBody)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

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

	return c.httpClient.Do(req)
}

func (c *Client) GetVideo(ctx context.Context, videoID string) (*Video, error) {
	resp, err := c.doRequest(ctx, http.MethodGet, "/videos/"+videoID, 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 video: status %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

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

	return &video, nil
}

func (c *Client) CreateUpload(ctx context.Context, req *UploadRequest) (*UploadResponse, error) {
	resp, err := c.doRequest(ctx, http.MethodPost, "/me/videos", req)
	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 upload: status %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

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

	return &uploadResp, nil
}

func (c *Client) CreatePullUpload(ctx context.Context, name, description, sourceURL string, fileSize int64) (*UploadResponse, error) {
	req := &UploadRequest{
		Name:        name,
		Description: description,
		Upload: UploadParams{
			Approach: "pull",
			Size:     fileSize,
			Link:     sourceURL,
		},
		Privacy: &PrivacyParams{
			View:  "unlisted",
			Embed: "public",
		},
	}
	return c.CreateUpload(ctx, req)
}

func (c *Client) CreateTusUpload(ctx context.Context, name, description string, fileSize int64) (*UploadResponse, error) {
	req := &UploadRequest{
		Name:        name,
		Description: description,
		Upload: UploadParams{
			Approach: "tus",
			Size:     fileSize,
		},
		Privacy: &PrivacyParams{
			View:  "unlisted",
			Embed: "public",
		},
	}
	return c.CreateUpload(ctx, req)
}

func (c *Client) UploadTusVideoFile(ctx context.Context, uploadLink string, fileData io.Reader, fileSize int64) error {
	uploadClient := &http.Client{
		Timeout: 30 * time.Minute,
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPatch, uploadLink, fileData)
	if err != nil {
		return fmt.Errorf("failed to create TUS upload request: %w", err)
	}

	req.Header.Set("Tus-Resumable", "1.0.0")
	req.Header.Set("Upload-Offset", "0")
	req.Header.Set("Content-Type", "application/offset+octet-stream")
	req.ContentLength = fileSize

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

	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("TUS upload failed: status %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	offsetStr := resp.Header.Get("Upload-Offset")
	if offsetStr != "" {
		offset, err := strconv.ParseInt(offsetStr, 10, 64)
		if err == nil && offset < fileSize {
			return fmt.Errorf("incomplete upload: uploaded %d of %d bytes", offset, fileSize)
		}
	}

	return nil
}

func (c *Client) UpdateVideo(ctx context.Context, videoID string, req *UpdateVideoRequest) (*Video, error) {
	resp, err := c.doRequest(ctx, http.MethodPatch, "/videos/"+videoID, req)
	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 update video: status %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

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

	return &video, nil
}

func (c *Client) DeleteVideo(ctx context.Context, videoID string) error {
	resp, err := c.doRequest(ctx, http.MethodDelete, "/videos/"+videoID, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

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

	return nil
}

func (c *Client) GetTranscodeStatus(ctx context.Context, videoID string) (string, error) {
	video, err := c.GetVideo(ctx, videoID)
	if err != nil {
		return "", err
	}

	if video.Transcode != nil {
		return video.Transcode.Status, nil
	}
	return "unknown", nil
}

func GetOEmbed(ctx context.Context, vimeoURL string, width, height int) (*OEmbedResponse, error) {
	client := &http.Client{Timeout: 10 * time.Second}

	oembedURL := fmt.Sprintf("https://vimeo.com/api/oembed.json?url=%s", vimeoURL)
	if width > 0 {
		oembedURL += "&width=" + strconv.Itoa(width)
	}
	if height > 0 {
		oembedURL += "&height=" + strconv.Itoa(height)
	}

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

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

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

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

	return &oembed, nil
}

func GenerateEmbedURL(videoID string, options *EmbedOptions) string {
	url := fmt.Sprintf("https://player.vimeo.com/video/%s", videoID)

	if options == nil {
		return url
	}

	params := ""
	if options.Autoplay {
		params += "&autoplay=1"
	}
	if options.Loop {
		params += "&loop=1"
	}
	if options.Muted {
		params += "&muted=1"
	}
	if !options.Title {
		params += "&title=0"
	}
	if !options.Byline {
		params += "&byline=0"
	}
	if !options.Portrait {
		params += "&portrait=0"
	}
	if options.Color != "" {
		params += "&color=" + options.Color
	}
	if options.Background {
		params += "&background=1"
	}
	if options.Responsive {
		params += "&responsive=1"
	}

	if params != "" {
		url += "?" + params[1:]
	}

	return url
}

type EmbedOptions struct {
	Autoplay   bool
	Loop       bool
	Muted      bool
	Title      bool
	Byline     bool
	Portrait   bool
	Color      string
	Background bool
	Responsive bool
}

func GenerateIframeEmbed(videoID string, width, height int, options *EmbedOptions) string {
	embedURL := GenerateEmbedURL(videoID, options)
	return fmt.Sprintf(
		`<iframe src="%s" width="%d" height="%d" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>`,
		embedURL, width, height,
	)
}

func ExtractVideoID(vimeoURL string) string {
	// Handle URLs like:
	// - https://vimeo.com/123456789
	// - https://player.vimeo.com/video/123456789
	// - /videos/123456789

	patterns := []string{
		"vimeo.com/",
		"player.vimeo.com/video/",
		"/videos/",
	}

	for _, pattern := range patterns {
		if idx := len(pattern); len(vimeoURL) > idx {
			for i := 0; i < len(vimeoURL)-len(pattern)+1; i++ {
				if vimeoURL[i:i+len(pattern)] == pattern {
					videoID := ""
					for j := i + len(pattern); j < len(vimeoURL); j++ {
						if vimeoURL[j] >= '0' && vimeoURL[j] <= '9' {
							videoID += string(vimeoURL[j])
						} else {
							break
						}
					}
					if videoID != "" {
						return videoID
					}
				}
			}
		}
	}

	return ""
}
