package githubreleases

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"text/template"
	"time"

	"github.com/Eriyc/rules_wails/pkg/wails3kit/updates"
)

const (
	defaultAssetTemplate         = "{{ .ProductID }}_{{ .Version }}_{{ .OS }}_{{ .Arch }}.zip"
	defaultChecksumAssetTemplate = "{{ .AssetName }}.sha256"
)

type Config struct {
	Owner                    string
	Repo                     string
	HTTPClient               *http.Client
	PrepareRequest           func(*http.Request) error
	AssetNameTemplate        string
	ChecksumAssetNameTemplate string
}

type Provider struct {
	config              Config
	client              *http.Client
	assetNameTemplate   *template.Template
	checksumNameTemplate *template.Template
	baseURL             string
}

func New(cfg Config) (*Provider, error) {
	if cfg.Owner == "" || cfg.Repo == "" {
		return nil, updates.ErrInvalidConfig
	}
	client := cfg.HTTPClient
	if client == nil {
		client = http.DefaultClient
	}
	assetTemplate := cfg.AssetNameTemplate
	if assetTemplate == "" {
		assetTemplate = defaultAssetTemplate
	}
	checksumTemplate := cfg.ChecksumAssetNameTemplate
	if checksumTemplate == "" {
		checksumTemplate = defaultChecksumAssetTemplate
	}

	assetNameTemplate, err := template.New("asset").Parse(assetTemplate)
	if err != nil {
		return nil, err
	}
	checksumNameTemplate, err := template.New("checksum").Parse(checksumTemplate)
	if err != nil {
		return nil, err
	}

	return &Provider{
		config:               cfg,
		client:               client,
		assetNameTemplate:    assetNameTemplate,
		checksumNameTemplate: checksumNameTemplate,
		baseURL:              "https://api.github.com",
	}, nil
}

type githubRelease struct {
	TagName     string        `json:"tag_name"`
	Body        string        `json:"body"`
	Prerelease  bool          `json:"prerelease"`
	PublishedAt time.Time     `json:"published_at"`
	Assets      []githubAsset `json:"assets"`
}

type githubAsset struct {
	Name               string `json:"name"`
	BrowserDownloadURL string `json:"browser_download_url"`
}

func (provider *Provider) Resolve(ctx context.Context, req updates.ResolveRequest) (*updates.Release, error) {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, provider.releaseURL(), nil)
	if err != nil {
		return nil, err
	}
	if provider.config.PrepareRequest != nil {
		if err := provider.config.PrepareRequest(request); err != nil {
			return nil, err
		}
	}

	response, err := provider.client.Do(request)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()

	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return nil, fmt.Errorf("unexpected github status: %s", response.Status)
	}

	var releases []githubRelease
	if err := json.NewDecoder(response.Body).Decode(&releases); err != nil {
		return nil, err
	}

	var best *updates.Release
	for _, release := range releases {
		version := strings.TrimPrefix(release.TagName, "v")
		channel := mapChannel(release)
		if channel != req.App.Channel {
			continue
		}
		comparison, err := updates.CompareVersions(version, req.App.CurrentVersion)
		if err != nil || comparison <= 0 {
			continue
		}

		assetName, err := provider.renderAssetName(req.App, version, "")
		if err != nil {
			return nil, err
		}
		checksumName, err := provider.renderChecksumName(req.App, version, assetName)
		if err != nil {
			return nil, err
		}

		artifactURL := ""
		checksumURL := ""
		for _, asset := range release.Assets {
			if asset.Name == assetName {
				artifactURL = asset.BrowserDownloadURL
			}
			if asset.Name == checksumName {
				checksumURL = asset.BrowserDownloadURL
			}
		}
		if artifactURL == "" || checksumURL == "" {
			continue
		}

		sha256Value, err := provider.readChecksum(ctx, checksumURL)
		if err != nil {
			return nil, err
		}

		candidate := &updates.Release{
			ID:            version,
			Version:       version,
			Channel:       channel,
			NotesMarkdown: release.Body,
			PublishedAt:   release.PublishedAt,
			Artifact: updates.Artifact{
				Kind:   updates.ArtifactKindBundleArchive,
				Format: updates.ArtifactFormatZip,
				URL:    artifactURL,
				SHA256: sha256Value,
			},
		}
		if best == nil {
			best = candidate
			continue
		}
		better, err := updates.CompareVersions(candidate.Version, best.Version)
		if err != nil {
			return nil, err
		}
		if better > 0 {
			best = candidate
		}
	}

	return best, nil
}

func (provider *Provider) OpenArtifact(ctx context.Context, release updates.Release) (io.ReadCloser, error) {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, release.Artifact.URL, nil)
	if err != nil {
		return nil, err
	}
	if provider.config.PrepareRequest != nil {
		if err := provider.config.PrepareRequest(request); err != nil {
			return nil, err
		}
	}
	response, err := provider.client.Do(request)
	if err != nil {
		return nil, err
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		defer response.Body.Close()
		return nil, fmt.Errorf("unexpected artifact status: %s", response.Status)
	}
	return response.Body, nil
}

func (provider *Provider) readChecksum(ctx context.Context, url string) (string, error) {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return "", err
	}
	if provider.config.PrepareRequest != nil {
		if err := provider.config.PrepareRequest(request); err != nil {
			return "", err
		}
	}
	response, err := provider.client.Do(request)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return "", fmt.Errorf("unexpected checksum status: %s", response.Status)
	}
	bytes, err := io.ReadAll(response.Body)
	if err != nil {
		return "", err
	}
	return strings.Fields(string(bytes))[0], nil
}

func (provider *Provider) releaseURL() string {
	return fmt.Sprintf("%s/repos/%s/%s/releases", provider.baseURL, provider.config.Owner, provider.config.Repo)
}

func (provider *Provider) renderAssetName(app updates.AppDescriptor, version string, assetName string) (string, error) {
	return provider.executeTemplate(provider.assetNameTemplate, app, version, assetName)
}

func (provider *Provider) renderChecksumName(app updates.AppDescriptor, version string, assetName string) (string, error) {
	return provider.executeTemplate(provider.checksumNameTemplate, app, version, assetName)
}

func (provider *Provider) executeTemplate(templateValue *template.Template, app updates.AppDescriptor, version string, assetName string) (string, error) {
	var builder strings.Builder
	err := templateValue.Execute(&builder, map[string]string{
		"ProductID": app.ProductID,
		"Version":   version,
		"OS":        app.OS,
		"Arch":      app.Arch,
		"AssetName": assetName,
	})
	return builder.String(), err
}

func mapChannel(release githubRelease) updates.Channel {
	if !release.Prerelease {
		return updates.ChannelStable
	}
	if strings.Contains(strings.ToLower(release.TagName), "alpha") {
		return updates.ChannelAlpha
	}
	return updates.ChannelBeta
}
