package main

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

const (
	apiRequestTimeout = 45 * time.Second
	maxHTTPAttempts   = 5
)

// API responses structures
type CommonResponse struct {
	Status  bool            `json:"status"`
	Message string          `json:"message"`
	Data    json.RawMessage `json:"data"`
	Errors  any             `json:"errors"`
}

type AuthResponse struct {
	AccessToken string `json:"access_token"`
}

type PreSignedResponse struct {
	TokenID       string            `json:"token_id"`
	UploadUrl     string            `json:"upload_url"`
	StorageKey    string            `json:"storage_key"`
	SignedHeaders map[string]string `json:"signed_headers"`
}

type MediaResponse struct {
	ID           string `json:"id"`
	StorageKey   string `json:"storage_key"`
	OriginalName string `json:"original_name"`
	MimeType     string `json:"mime_type"`
	Size         int64  `json:"size"`
}

type PreSignedCompleteDto struct {
	TokenID      string          `json:"token_id"`
	FileMetadata json.RawMessage `json:"file_metadata,omitempty"`
}

type CreateComponentRequest struct {
	Type        string   `json:"type"`
	Platform    string   `json:"platform"`
	Status      string   `json:"status"`
	Version     string   `json:"version"`
	Description *string  `json:"description,omitempty"`
	Hash        *string  `json:"hash,omitempty"`
	MediaIDs    []string `json:"media_ids"`
	GameIDs     []string `json:"game_ids"`
}

type UpdateComponentRequest struct {
	Type        *string  `json:"type,omitempty"`
	Platform    *string  `json:"platform,omitempty"`
	Status      *string  `json:"status,omitempty"`
	Version     *string  `json:"version,omitempty"`
	Description *string  `json:"description,omitempty"`
	Hash        *string  `json:"hash,omitempty"`
	MediaIDs    []string `json:"media_ids,omitempty"`
	GameIDs     []string `json:"game_ids,omitempty"`
}

func readFile(path string) string {
	data, err := os.ReadFile(path)
	if err != nil {
		panic(fmt.Sprintf("Failed to read %s: %v", path, err))
	}
	return string(data)
}

func main() {
	// Flag definitions (useful for manual testing, but optional in CI/CD)
	apiURLFlag := flag.String("api-url", "https://api.punklorde.org", "Base URL of the management API")
	gameIdsFlag := flag.String("game-ids", "", "Comma-separated Game IDs (defaults to ENV_GAME_IDS)")
	filesFlag := flag.String("files", "", "Comma-separated list of files to upload (defaults to scanning prebuild/)")
	cTypeFlag := flag.String("type", "", "Component type: LAUNCHER, PROXY, SERVER (defaults to ENV_COMPONENT_TYPE or PROXY)")

	flag.Parse()

	// 1. Resolve settings from Env or Flags
	apiURL := *apiURLFlag
	if envAPI := os.Getenv("ENV_API_URL"); envAPI != "" {
		apiURL = envAPI
	}

	gameIdsStr := *gameIdsFlag
	if gameIdsStr == "" {
		gameIdsStr = os.Getenv("ENV_GAME_IDS")
	}
	if gameIdsStr == "" {
		fmt.Fprintln(os.Stderr, "Error: game IDs must be specified via -game-ids flag or ENV_GAME_IDS environment variable")
		os.Exit(1)
	}
	gameIDs := splitCommaSeparated(gameIdsStr)

	cType := *cTypeFlag
	if cType == "" {
		cType = os.Getenv("ENV_COMPONENT_TYPE")
	}
	if cType == "" {
		cType = "PROXY" // Default component type
	}

	// 2. Read release metadata from files
	releaseJSON := readFile("script/release.json")
	var meta map[string]string
	if err := json.Unmarshal([]byte(releaseJSON), &meta); err != nil {
		fmt.Fprintf(os.Stderr, "Failed to parse release.json: %v\n", err)
		os.Exit(1)
	}
	version := meta["tag"]
	if version == "" {
		fmt.Fprintln(os.Stderr, "Error: 'tag' is missing in release.json")
		os.Exit(1)
	}

	var description *string
	if bodyBytes, err := os.ReadFile("script/README_Note.md"); err == nil && len(bodyBytes) > 0 {
		descStr := string(bodyBytes)
		description = &descStr
	}

	// 3. Read robot token from environment
	robotToken := os.Getenv("ENV_ROBOT_TOKEN")
	if robotToken == "" {
		fmt.Fprintln(os.Stderr, "Error: ENV_ROBOT_TOKEN environment variable is missing")
		os.Exit(1)
	}

	fmt.Println("Refreshing access token using robot token...")
	accessToken, err := refreshRobotToken(apiURL, robotToken)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to refresh token: %v\n", err)
		os.Exit(1)
	}
	fmt.Println("Access token successfully obtained.")

	// 4. Resolve files to publish
	var filePaths []string
	filesStr := *filesFlag
	if filesStr == "" {
		filesStr = os.Getenv("ENV_FILES")
	}

	if filesStr != "" {
		filePaths = splitCommaSeparated(filesStr)
	} else {
		// Fallback to scanning prebuild/
		prebuildDir := "prebuild"
		files, err := os.ReadDir(prebuildDir)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Cannot read prebuild folder: %v\n", err)
			os.Exit(1)
		}
		for _, file := range files {
			if !file.IsDir() && filepath.Ext(file.Name()) == ".zip" {
				filePaths = append(filePaths, filepath.Join(prebuildDir, file.Name()))
			}
		}
	}

	var processedCount int
	for _, filePath := range filePaths {
		fileName := filepath.Base(filePath)
		fmt.Printf("\n--- Processing asset: %s ---\n", fileName)

		// Map filename to platform
		platform := detectPlatformFromFilename(fileName)
		fmt.Printf("Mapped Platform: %s\n", platform)

		// Calculate size and SHA256
		size, hash, err := getFileInfoAndHash(filePath)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", fileName, err)
			os.Exit(1)
		}
		fmt.Printf("Size: %d bytes, SHA256: %s\n", size, hash)

		// Request presigned URL
		contentType := "application/octet-stream"
		if filepath.Ext(fileName) == ".zip" {
			contentType = "application/zip"
		}
		fmt.Println("Requesting presigned URL...")
		presigned, err := getPresignedURL(apiURL, accessToken, fileName, contentType, size)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to get presigned URL: %v\n", err)
			os.Exit(1)
		}

		// Upload file to S3
		fmt.Println("Uploading file to storage...")
		err = uploadFileToS3(filePath, presigned)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to upload file to storage: %v\n", err)
			os.Exit(1)
		}

		// Confirm upload completion
		fmt.Println("Confirming upload completion...")
		mediaID, err := completePreSignedUpload(apiURL, accessToken, presigned.TokenID, hash)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to complete upload: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("Media ID generated: %s\n", mediaID)

		fmt.Println("Checking if component already exists...")
		existingID, err := findExistingComponent(apiURL, accessToken, cType, platform, version)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Warning: Failed to check existing component: %v\n", err)
		}

		var compID string
		if existingID != "" {
			fmt.Printf("Component already exists with ID: %s. Updating it...\n", existingID)
			updateReq := UpdateComponentRequest{
				Type:        &cType,
				Platform:    &platform,
				Description: description,
				Hash:        &hash,
				MediaIDs:    []string{mediaID},
				GameIDs:     gameIDs,
			}
			compID, err = updateComponent(apiURL, accessToken, existingID, updateReq)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Failed to update component: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("SUCCESS! Component updated with ID: %s for Platform: %s\n", compID, platform)
		} else {
			fmt.Println("Component does not exist. Registering on the API...")
			reqBody := CreateComponentRequest{
				Type:        cType,
				Platform:    platform,
				Status:      "ACTIVE",
				Version:     version,
				Description: description,
				Hash:        &hash,
				MediaIDs:    []string{mediaID},
				GameIDs:     gameIDs,
			}
			compID, err = createComponent(apiURL, accessToken, reqBody)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Failed to register component: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("SUCCESS! Component created with ID: %s for Platform: %s\n", compID, platform)
		}
		processedCount++
	}

	if processedCount == 0 {
		fmt.Println("No component files found to publish.")
	} else {
		fmt.Printf("\nAll %d components successfully published to API.\n", processedCount)
	}
}

func detectPlatformFromFilename(name string) string {
	nameLower := strings.ToLower(name)
	if strings.Contains(nameLower, "mac_arm") || strings.Contains(nameLower, "macos-arm64") {
		return "MACOS_ARM64"
	}
	if strings.Contains(nameLower, "mac_x86") || strings.Contains(nameLower, "macos-amd64") {
		return "MACOS_X64"
	}
	if strings.Contains(nameLower, "win_arm") || strings.Contains(nameLower, "win-arm") || strings.Contains(nameLower, "windows-arm") {
		return "WINDOWS_ARM64"
	}
	if strings.Contains(nameLower, "win_x86") || strings.Contains(nameLower, "win_x64") || strings.Contains(nameLower, "win") {
		return "WINDOWS_X64"
	}
	if strings.Contains(nameLower, "android_arm64") {
		return "ANDROID_ARM64"
	}
	if strings.Contains(nameLower, "linux_x64") || strings.Contains(nameLower, "linux-amd64") || strings.Contains(nameLower, "linux-x64") {
		return "LINUX_X64"
	}
	if strings.Contains(nameLower, "linux_arm64") || strings.Contains(nameLower, "linux-arm64") {
		return "LINUX_ARM64"
	}
	return "WINDOWS_X64" // Fallback default
}

func splitCommaSeparated(s string) []string {
	parts := strings.Split(s, ",")
	var result []string
	for _, p := range parts {
		trimmed := strings.TrimSpace(p)
		if trimmed != "" {
			result = append(result, trimmed)
		}
	}
	return result
}

func getFileInfoAndHash(path string) (int64, string, error) {
	file, err := os.Open(path)
	if err != nil {
		return 0, "", err
	}
	defer file.Close()

	stat, err := file.Stat()
	if err != nil {
		return 0, "", err
	}

	hasher := sha256.New()
	if _, err := io.Copy(hasher, file); err != nil {
		return 0, "", err
	}

	return stat.Size(), hex.EncodeToString(hasher.Sum(nil)), nil
}

func apiEndpoint(apiURL, path string) string {
	return strings.TrimRight(apiURL, "/") + path
}

func retryDelay(attempt int) time.Duration {
	delay := time.Duration(1<<uint(attempt-1)) * time.Second
	if delay > 20*time.Second {
		return 20 * time.Second
	}
	return delay
}

func isRetryableStatus(statusCode int) bool {
	return statusCode == http.StatusRequestTimeout ||
		statusCode == http.StatusTooEarly ||
		statusCode == http.StatusTooManyRequests ||
		statusCode >= http.StatusInternalServerError
}

func isAccepted(statusCode int, accepted ...int) bool {
	for _, code := range accepted {
		if statusCode == code {
			return true
		}
	}
	return false
}

func doHTTPWithRetry(label string, client *http.Client, buildRequest func() (*http.Request, error), accepted ...int) ([]byte, int, error) {
	var lastErr error
	for attempt := 1; attempt <= maxHTTPAttempts; attempt++ {
		req, err := buildRequest()
		if err != nil {
			return nil, 0, err
		}

		resp, err := client.Do(req)
		if err != nil {
			lastErr = err
			if attempt < maxHTTPAttempts {
				delay := retryDelay(attempt)
				fmt.Fprintf(os.Stderr, "%s attempt %d/%d failed: %v. Retrying in %s...\n", label, attempt, maxHTTPAttempts, err, delay)
				time.Sleep(delay)
				continue
			}
			return nil, 0, err
		}

		bodyBytes, readErr := io.ReadAll(resp.Body)
		_ = resp.Body.Close()
		if readErr != nil {
			return nil, resp.StatusCode, readErr
		}

		if isAccepted(resp.StatusCode, accepted...) {
			return bodyBytes, resp.StatusCode, nil
		}

		if !isRetryableStatus(resp.StatusCode) || attempt == maxHTTPAttempts {
			return bodyBytes, resp.StatusCode, nil
		}

		lastErr = fmt.Errorf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(bodyBytes)))
		delay := retryDelay(attempt)
		fmt.Fprintf(os.Stderr, "%s attempt %d/%d returned %v. Retrying in %s...\n", label, attempt, maxHTTPAttempts, lastErr, delay)
		time.Sleep(delay)
	}

	return nil, 0, lastErr
}

func refreshRobotToken(apiURL, robotToken string) (string, error) {
	endpoint := apiEndpoint(apiURL, "/robot-tokens/refresh")
	client := &http.Client{Timeout: apiRequestTimeout}
	bodyBytes, statusCode, err := doHTTPWithRetry("refresh robot token", client, func() (*http.Request, error) {
		req, err := http.NewRequest("POST", endpoint, nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+robotToken)
		return req, nil
	}, http.StatusOK)
	if err != nil {
		return "", err
	}
	if statusCode != http.StatusOK {
		return "", fmt.Errorf("refresh token failed with status %d: %s", statusCode, string(bodyBytes))
	}

	var cr CommonResponse
	if err := json.Unmarshal(bodyBytes, &cr); err != nil {
		return "", fmt.Errorf("failed to parse common response: %w", err)
	}

	if !cr.Status {
		return "", fmt.Errorf("API error: %s", cr.Message)
	}

	var auth AuthResponse
	if err := json.Unmarshal(cr.Data, &auth); err != nil {
		return "", fmt.Errorf("failed to parse auth response data: %w", err)
	}

	return auth.AccessToken, nil
}

func getPresignedURL(apiURL, accessToken, fileName, contentType string, size int64) (*PreSignedResponse, error) {
	endpoint, err := url.Parse(apiEndpoint(apiURL, "/media/presigned"))
	if err != nil {
		return nil, err
	}
	query := endpoint.Query()
	query.Set("fileName", fileName)
	query.Set("content_type", contentType)
	query.Set("size", strconv.FormatInt(size, 10))
	endpoint.RawQuery = query.Encode()

	client := &http.Client{Timeout: apiRequestTimeout}
	bodyBytes, statusCode, err := doHTTPWithRetry("get presigned URL", client, func() (*http.Request, error) {
		req, err := http.NewRequest("GET", endpoint.String(), nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+accessToken)
		return req, nil
	}, http.StatusOK)
	if err != nil {
		return nil, err
	}
	if statusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to get presigned URL (status %d): %s", statusCode, string(bodyBytes))
	}

	var cr CommonResponse
	if err := json.Unmarshal(bodyBytes, &cr); err != nil {
		return nil, fmt.Errorf("failed to parse common response: %w", err)
	}

	if !cr.Status {
		return nil, fmt.Errorf("API error: %s", cr.Message)
	}

	var presigned PreSignedResponse
	if err := json.Unmarshal(cr.Data, &presigned); err != nil {
		return nil, fmt.Errorf("failed to parse presigned data: %w", err)
	}

	return &presigned, nil
}

func uploadFileToS3(path string, presigned *PreSignedResponse) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	stat, err := file.Stat()
	if err != nil {
		return err
	}

	req, err := http.NewRequest("PUT", presigned.UploadUrl, file)
	if err != nil {
		return err
	}

	// Set S3 signature headers
	for k, v := range presigned.SignedHeaders {
		req.Header.Set(k, v)
	}

	req.ContentLength = stat.Size()
	if req.Header.Get("Content-Type") == "" {
		req.Header.Set("Content-Type", "application/zip")
	}

	client := &http.Client{Timeout: 10 * time.Minute}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

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

	return nil
}

func completePreSignedUpload(apiURL, accessToken, tokenID, hash string) (string, error) {
	endpoint := apiEndpoint(apiURL, "/media/presigned/complete")

	metaJSON, _ := json.Marshal(map[string]string{"sha256": hash})
	dto := PreSignedCompleteDto{
		TokenID:      tokenID,
		FileMetadata: json.RawMessage(metaJSON),
	}

	payload, _ := json.Marshal(dto)

	client := &http.Client{Timeout: apiRequestTimeout}
	bodyBytes, statusCode, err := doHTTPWithRetry("complete presigned upload", client, func() (*http.Request, error) {
		req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+accessToken)
		req.Header.Set("Content-Type", "application/json")
		return req, nil
	}, http.StatusOK)
	if err != nil {
		return "", err
	}
	if statusCode != http.StatusOK {
		return "", fmt.Errorf("complete upload failed (status %d): %s", statusCode, string(bodyBytes))
	}

	var cr CommonResponse
	if err := json.Unmarshal(bodyBytes, &cr); err != nil {
		return "", fmt.Errorf("failed to parse common response: %w", err)
	}

	if !cr.Status {
		return "", fmt.Errorf("API error: %s", cr.Message)
	}

	var media MediaResponse
	if err := json.Unmarshal(cr.Data, &media); err != nil {
		return "", fmt.Errorf("failed to parse media data: %w", err)
	}

	return media.ID, nil
}

func createComponent(apiURL, accessToken string, dto CreateComponentRequest) (string, error) {
	endpoint := apiEndpoint(apiURL, "/components")

	payload, _ := json.Marshal(dto)

	client := &http.Client{Timeout: apiRequestTimeout}
	bodyBytes, statusCode, err := doHTTPWithRetry("create component", client, func() (*http.Request, error) {
		req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+accessToken)
		req.Header.Set("Content-Type", "application/json")
		return req, nil
	}, http.StatusCreated, http.StatusOK)
	if err != nil {
		return "", err
	}
	if statusCode != http.StatusCreated && statusCode != http.StatusOK {
		return "", fmt.Errorf("create component failed (status %d): %s", statusCode, string(bodyBytes))
	}

	var cr CommonResponse
	if err := json.Unmarshal(bodyBytes, &cr); err != nil {
		return "", fmt.Errorf("failed to parse common response: %w", err)
	}

	if !cr.Status {
		return "", fmt.Errorf("API error: %s", cr.Message)
	}

	var component struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal(cr.Data, &component); err != nil {
		return "", fmt.Errorf("failed to parse component data: %w", err)
	}

	return component.ID, nil
}

func findExistingComponent(apiURL, accessToken, cType, platform, version string) (string, error) {
	endpoint, err := url.Parse(apiEndpoint(apiURL, "/components"))
	if err != nil {
		return "", err
	}
	query := endpoint.Query()
	query.Set("type", cType)
	query.Set("platform", platform)
	query.Set("search", version)
	endpoint.RawQuery = query.Encode()

	client := &http.Client{Timeout: apiRequestTimeout}
	bodyBytes, statusCode, err := doHTTPWithRetry("search existing component", client, func() (*http.Request, error) {
		req, err := http.NewRequest("GET", endpoint.String(), nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+accessToken)
		return req, nil
	}, http.StatusOK)
	if err != nil {
		return "", err
	}
	if statusCode != http.StatusOK {
		return "", fmt.Errorf("failed to search components (status %d): %s", statusCode, string(bodyBytes))
	}

	var pr struct {
		Status bool `json:"status"`
		Data   []struct {
			ID            string `json:"id"`
			ComponentType string `json:"component_type"`
			Platform      string `json:"platform"`
			Version       string `json:"version"`
		} `json:"data"`
	}

	if err := json.Unmarshal(bodyBytes, &pr); err != nil {
		return "", fmt.Errorf("failed to parse components search response: %w", err)
	}

	if !pr.Status {
		return "", fmt.Errorf("search components returned unsuccessful status")
	}

	for _, item := range pr.Data {
		if item.ComponentType == cType && item.Platform == platform && item.Version == version {
			return item.ID, nil
		}
	}

	return "", nil
}

func updateComponent(apiURL, accessToken, id string, dto UpdateComponentRequest) (string, error) {
	endpoint := apiEndpoint(apiURL, "/components/"+url.PathEscape(id))

	payload, _ := json.Marshal(dto)

	client := &http.Client{Timeout: apiRequestTimeout}
	bodyBytes, statusCode, err := doHTTPWithRetry("update component", client, func() (*http.Request, error) {
		req, err := http.NewRequest("PUT", endpoint, bytes.NewReader(payload))
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+accessToken)
		req.Header.Set("Content-Type", "application/json")
		return req, nil
	}, http.StatusOK)
	if err != nil {
		return "", err
	}
	if statusCode != http.StatusOK {
		return "", fmt.Errorf("update component failed (status %d): %s", statusCode, string(bodyBytes))
	}

	var cr CommonResponse
	if err := json.Unmarshal(bodyBytes, &cr); err != nil {
		return "", fmt.Errorf("failed to parse common response: %w", err)
	}

	if !cr.Status {
		return "", fmt.Errorf("API error: %s", cr.Message)
	}

	var component struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal(cr.Data, &component); err != nil {
		return "", fmt.Errorf("failed to parse component data: %w", err)
	}

	return component.ID, nil
}
