package virustotal

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

const (
	defaultBaseURL    = "https://www.virustotal.com/api/v3"
	directUploadLimit = 32 * 1024 * 1024
	maxUploadLimit    = 650 * 1024 * 1024
)

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

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

func (c *Client) apiURL(path string) string {
	return strings.TrimRight(c.baseURL, "/") + path
}

type analysisStats struct {
	Malicious  int `json:"malicious"`
	Suspicious int `json:"suspicious"`
}

type fileReport struct {
	Data struct {
		Attributes struct {
			LastAnalysisStats analysisStats `json:"last_analysis_stats"`
		} `json:"attributes"`
	} `json:"data"`
}

type analysisResponse struct {
	Data struct {
		Attributes struct {
			Status string        `json:"status"`
			Stats  analysisStats `json:"stats"`
		} `json:"attributes"`
	} `json:"data"`
}

type uploadResponse struct {
	Data struct {
		ID string `json:"id"`
	} `json:"data"`
}

type uploadURLResponse struct {
	Data string `json:"data"`
}

func (c *Client) headers() http.Header {
	h := http.Header{}
	h.Set("Accept", "application/json")
	h.Set("x-apikey", c.apiKey)
	return h
}

func (c *Client) lookupFile(ctx context.Context, sha256 string) (analysisStats, bool, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiURL("/files/"+sha256), nil)
	if err != nil {
		return analysisStats{}, false, err
	}
	req.Header = c.headers()

	res, err := c.httpClient.Do(req)
	if err != nil {
		return analysisStats{}, false, err
	}
	defer res.Body.Close()

	if res.StatusCode == http.StatusNotFound {
		return analysisStats{}, false, nil
	}
	if res.StatusCode >= 500 || res.StatusCode == http.StatusTooManyRequests {
		return analysisStats{}, false, fmt.Errorf("virustotal lookup unavailable: %d", res.StatusCode)
	}
	if res.StatusCode >= 400 {
		return analysisStats{}, false, nil
	}

	var report fileReport
	if err := json.NewDecoder(res.Body).Decode(&report); err != nil {
		return analysisStats{}, false, err
	}
	return report.Data.Attributes.LastAnalysisStats, true, nil
}

func (c *Client) uploadFile(ctx context.Context, data []byte, filename string) (string, error) {
	if len(data) <= directUploadLimit {
		return c.uploadDirect(ctx, data, filename)
	}
	return c.uploadLarge(ctx, data, filename)
}

func (c *Client) uploadDirect(ctx context.Context, data []byte, filename string) (string, error) {
	var body bytes.Buffer
	w := multipart.NewWriter(&body)
	part, err := w.CreateFormFile("file", filename)
	if err != nil {
		return "", err
	}
	if _, err := part.Write(data); err != nil {
		return "", err
	}
	if err := w.Close(); err != nil {
		return "", err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURL("/files"), &body)
	if err != nil {
		return "", err
	}
	req.Header = c.headers()
	req.Header.Set("Content-Type", w.FormDataContentType())

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

	if res.StatusCode >= 400 {
		b, _ := io.ReadAll(io.LimitReader(res.Body, 4096))
		return "", fmt.Errorf("virustotal upload failed: %d %s", res.StatusCode, string(b))
	}

	var out uploadResponse
	if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
		return "", err
	}
	return out.Data.ID, nil
}

func (c *Client) uploadLarge(ctx context.Context, data []byte, filename string) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiURL("/files/upload_url"), nil)
	if err != nil {
		return "", err
	}
	req.Header = c.headers()

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

	if res.StatusCode >= 400 {
		b, _ := io.ReadAll(io.LimitReader(res.Body, 4096))
		return "", fmt.Errorf("virustotal upload_url failed: %d %s", res.StatusCode, string(b))
	}

	var urlResp uploadURLResponse
	if err := json.NewDecoder(res.Body).Decode(&urlResp); err != nil {
		return "", err
	}
	if urlResp.Data == "" {
		return "", fmt.Errorf("virustotal upload_url empty")
	}

	var body bytes.Buffer
	w := multipart.NewWriter(&body)
	part, err := w.CreateFormFile("file", filename)
	if err != nil {
		return "", err
	}
	if _, err := part.Write(data); err != nil {
		return "", err
	}
	if err := w.Close(); err != nil {
		return "", err
	}

	upReq, err := http.NewRequestWithContext(ctx, http.MethodPost, urlResp.Data, &body)
	if err != nil {
		return "", err
	}
	upReq.Header = c.headers()
	upReq.Header.Set("Content-Type", w.FormDataContentType())

	upRes, err := c.httpClient.Do(upReq)
	if err != nil {
		return "", err
	}
	defer upRes.Body.Close()

	if upRes.StatusCode >= 400 {
		b, _ := io.ReadAll(io.LimitReader(upRes.Body, 4096))
		return "", fmt.Errorf("virustotal large upload failed: %d %s", upRes.StatusCode, string(b))
	}

	var out uploadResponse
	if err := json.NewDecoder(upRes.Body).Decode(&out); err != nil {
		return "", err
	}
	return out.Data.ID, nil
}

func (c *Client) pollAnalysis(ctx context.Context, analysisID string, timeout time.Duration) (analysisStats, error) {
	deadline := time.Now().Add(timeout)
	for {
		if ctx.Err() != nil {
			return analysisStats{}, ctx.Err()
		}
		if time.Now().After(deadline) {
			return analysisStats{}, fmt.Errorf("virustotal analysis timeout")
		}

		req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiURL("/analyses/"+analysisID), nil)
		if err != nil {
			return analysisStats{}, err
		}
		req.Header = c.headers()

		res, err := c.httpClient.Do(req)
		if err != nil {
			return analysisStats{}, err
		}

		var out analysisResponse
		decodeErr := json.NewDecoder(res.Body).Decode(&out)
		res.Body.Close()
		if decodeErr != nil {
			return analysisStats{}, decodeErr
		}

		if res.StatusCode >= 500 || res.StatusCode == http.StatusTooManyRequests {
			return analysisStats{}, fmt.Errorf("virustotal poll unavailable: %d", res.StatusCode)
		}

		status := out.Data.Attributes.Status
		if status == "completed" {
			return out.Data.Attributes.Stats, nil
		}

		select {
		case <-ctx.Done():
			return analysisStats{}, ctx.Err()
		case <-time.After(2 * time.Second):
		}
	}
}

func statsMalicious(stats analysisStats) bool {
	return stats.Malicious > 0
}
