package fidcli

import (
	"archive/zip"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/google/go-github/v54/github"
)

const (
	queryImageWorkflow = "query-image-tag.yml"
	queryImageRef      = "main"
	queryArtifactName  = "result.json"
)

var (
	queryRunLookupInterval = 2 * time.Second
	queryRunPollInterval   = 5 * time.Second
)

type ImageAvailabilityStatus string

const (
	ImageAvailabilityStatusAvailable   ImageAvailabilityStatus = "AVAILABLE"
	ImageAvailabilityStatusMissing     ImageAvailabilityStatus = "MISSING"
	ImageAvailabilityStatusAuthFailed  ImageAvailabilityStatus = "AUTH_FAILED"
	ImageAvailabilityStatusCheckFailed ImageAvailabilityStatus = "CHECK_FAILED"
)

type ImageAvailability struct {
	CloudProvider  string
	Registry       string
	ImageRef       string
	Status         ImageAvailabilityStatus
	Digest         string
	MultiArch      bool
	OCIPlatforms   []string
	ImageCreatedAt *time.Time
	CheckedAt      time.Time
	WorkflowRunID  int64
	WorkflowRunURL string
	ErrorMessage   string
}

// ImageAvailabilityQuery identifies one dispatched query-image-tag workflow.
// Callers can persist this small handle and poll the result without keeping a
// request or database transaction open while GitHub Actions is running.
type ImageAvailabilityQuery struct {
	ImageRef       string
	WorkflowRunID  int64
	WorkflowRunURL string
}

type queryArtifact struct {
	ImageRef  string   `json:"image_ref"`
	CreatedAt string   `json:"created_at"`
	Digest    string   `json:"digest"`
	MultiArch bool     `json:"multi_arch"`
	Platforms []string `json:"platforms"`
}

func (c *client) QueryImageAvailability(ctx context.Context, imageRef string) (*ImageAvailability, error) {
	query, err := c.StartImageAvailabilityQuery(ctx, imageRef)
	if err != nil {
		return nil, err
	}
	run, err := c.waitForQueryCompletion(ctx, query.WorkflowRunID)
	if err != nil {
		return nil, err
	}
	return c.availabilityFromRun(ctx, query.ImageRef, run)
}

func (c *client) StartImageAvailabilityQuery(ctx context.Context, imageRef string) (*ImageAvailabilityQuery, error) {
	if c.github == nil || c.downloadHTTPClient == nil {
		return nil, fmt.Errorf("FID client is disabled")
	}
	repository, tag, err := splitImageRef(imageRef)
	if err != nil {
		return nil, err
	}
	maxRunID, err := c.maxQueryRunID(ctx)
	if err != nil {
		return nil, err
	}
	dispatchedAt := time.Now().UTC()
	_, err = c.github.Actions.CreateWorkflowDispatchEventByFileName(
		ctx,
		c.owner,
		c.repo,
		queryImageWorkflow,
		github.CreateWorkflowDispatchEventRequest{
			Ref: queryImageRef,
			Inputs: map[string]interface{}{
				"registry_url": repository,
				"image_tag":    tag,
			},
		},
	)
	if err != nil {
		return nil, fmt.Errorf("dispatch image availability workflow: %w", err)
	}
	run, err := c.waitForQueryRun(ctx, imageRef, maxRunID, dispatchedAt)
	if err != nil {
		return nil, err
	}
	return &ImageAvailabilityQuery{
		ImageRef:       imageRef,
		WorkflowRunID:  run.GetID(),
		WorkflowRunURL: run.GetHTMLURL(),
	}, nil
}

func (c *client) GetImageAvailabilityQueryResult(
	ctx context.Context,
	query ImageAvailabilityQuery,
) (*ImageAvailability, bool, error) {
	if c.github == nil || c.downloadHTTPClient == nil {
		return nil, false, fmt.Errorf("FID client is disabled")
	}
	if strings.TrimSpace(query.ImageRef) == "" || query.WorkflowRunID <= 0 {
		return nil, false, fmt.Errorf("image availability query is incomplete")
	}
	run, _, err := c.github.Actions.GetWorkflowRunByID(ctx, c.owner, c.repo, query.WorkflowRunID)
	if err != nil {
		return nil, false, fmt.Errorf("get image availability workflow run: %w", err)
	}
	if run.GetStatus() != "completed" {
		return nil, false, nil
	}
	result, err := c.availabilityFromRun(ctx, query.ImageRef, run)
	if err != nil {
		return nil, false, err
	}
	return result, true, nil
}

func splitImageRef(imageRef string) (string, string, error) {
	trimmed := strings.TrimSpace(imageRef)
	separator := strings.LastIndex(trimmed, ":")
	if separator <= strings.LastIndex(trimmed, "/") || separator == len(trimmed)-1 {
		return "", "", fmt.Errorf("image reference must include a tag: %s", imageRef)
	}
	return trimmed[:separator], trimmed[separator+1:], nil
}

func CloudProviderFromImageRef(imageRef string) string {
	repository, _, err := splitImageRef(imageRef)
	if err != nil {
		return "UNKNOWN"
	}
	host := strings.ToLower(strings.SplitN(repository, "/", 2)[0])
	switch {
	case strings.Contains(host, ".dkr.ecr.") && strings.HasSuffix(host, ".amazonaws.com"):
		return "AWS"
	case strings.HasSuffix(host, ".cr.aliyuncs.com"):
		return "ALIYUN"
	case strings.HasSuffix(host, ".azurecr.io"):
		return "AZURE"
	default:
		return "UNKNOWN"
	}
}

func RegistryFromImageRef(imageRef string) string {
	repository, _, err := splitImageRef(imageRef)
	if err != nil {
		return ""
	}
	return strings.SplitN(repository, "/", 2)[0]
}

func (c *client) maxQueryRunID(ctx context.Context) (int64, error) {
	runs, _, err := c.github.Actions.ListWorkflowRunsByFileName(
		ctx,
		c.owner,
		c.repo,
		queryImageWorkflow,
		&github.ListWorkflowRunsOptions{ListOptions: github.ListOptions{PerPage: 1}},
	)
	if err != nil {
		return 0, fmt.Errorf("list image availability workflow runs: %w", err)
	}
	if runs == nil || len(runs.WorkflowRuns) == 0 || runs.WorkflowRuns[0] == nil {
		return 0, nil
	}
	return runs.WorkflowRuns[0].GetID(), nil
}

func (c *client) waitForQueryRun(ctx context.Context, imageRef string, minRunID int64, dispatchedAt time.Time) (*github.WorkflowRun, error) {
	ticker := time.NewTicker(queryRunLookupInterval)
	defer ticker.Stop()
	expectedTitle := "query-image-tag " + imageRef
	for {
		runs, _, err := c.github.Actions.ListWorkflowRunsByFileName(
			ctx,
			c.owner,
			c.repo,
			queryImageWorkflow,
			&github.ListWorkflowRunsOptions{
				Event:       "workflow_dispatch",
				ListOptions: github.ListOptions{PerPage: 20},
			},
		)
		if err != nil {
			return nil, fmt.Errorf("find dispatched image availability workflow: %w", err)
		}
		if runs != nil {
			for _, run := range runs.WorkflowRuns {
				if run == nil || run.GetID() <= minRunID || run.GetDisplayTitle() != expectedTitle {
					continue
				}
				if run.CreatedAt != nil && run.CreatedAt.Time.Before(dispatchedAt.Add(-time.Minute)) {
					continue
				}
				return run, nil
			}
		}
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-ticker.C:
		}
	}
}

func (c *client) waitForQueryCompletion(ctx context.Context, runID int64) (*github.WorkflowRun, error) {
	ticker := time.NewTicker(queryRunPollInterval)
	defer ticker.Stop()
	for {
		run, _, err := c.github.Actions.GetWorkflowRunByID(ctx, c.owner, c.repo, runID)
		if err != nil {
			return nil, fmt.Errorf("get image availability workflow run: %w", err)
		}
		if run.GetStatus() == "completed" {
			return run, nil
		}
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-ticker.C:
		}
	}
}

func (c *client) availabilityFromRun(ctx context.Context, imageRef string, run *github.WorkflowRun) (*ImageAvailability, error) {
	checkedAt := time.Now().UTC()
	result := &ImageAvailability{
		CloudProvider:  CloudProviderFromImageRef(imageRef),
		Registry:       RegistryFromImageRef(imageRef),
		ImageRef:       imageRef,
		CheckedAt:      checkedAt,
		WorkflowRunID:  run.GetID(),
		WorkflowRunURL: run.GetHTMLURL(),
	}
	if run.GetConclusion() != "success" {
		status, message := c.classifyQueryFailure(ctx, run)
		result.Status = status
		result.ErrorMessage = message
		return result, nil
	}
	artifact, err := c.downloadQueryArtifact(ctx, run.GetID())
	if err != nil {
		return nil, err
	}
	createdAt, err := time.Parse(time.RFC3339Nano, artifact.CreatedAt)
	if err != nil {
		return nil, fmt.Errorf("parse queried image created_at: %w", err)
	}
	result.Status = ImageAvailabilityStatusAvailable
	result.ImageRef = artifact.ImageRef
	result.Digest = artifact.Digest
	result.MultiArch = artifact.MultiArch
	result.OCIPlatforms = artifact.Platforms
	result.ImageCreatedAt = &createdAt
	return result, nil
}

func (c *client) classifyQueryFailure(ctx context.Context, run *github.WorkflowRun) (ImageAvailabilityStatus, string) {
	jobs, _, err := c.github.Actions.ListWorkflowJobs(
		ctx,
		c.owner,
		c.repo,
		run.GetID(),
		&github.ListWorkflowJobsOptions{Filter: "latest", ListOptions: github.ListOptions{PerPage: 100}},
	)
	if err != nil || jobs == nil {
		return ImageAvailabilityStatusCheckFailed, fmt.Sprintf("workflow concluded %s", run.GetConclusion())
	}
	for _, job := range jobs.Jobs {
		for _, step := range job.Steps {
			if step.GetConclusion() == "" || step.GetConclusion() == "success" || step.GetConclusion() == "skipped" {
				continue
			}
			if step.GetName() == "Authenticate registry" {
				return ImageAvailabilityStatusAuthFailed, "failed to authenticate to the target registry"
			}
			logs := c.queryJobLogs(ctx, job.GetID())
			if queryLogsLookNotFound(logs) {
				return ImageAvailabilityStatusMissing, "image tag was not found in the target registry"
			}
			if queryLogsLookAuthFailure(logs) {
				return ImageAvailabilityStatusAuthFailed, "the target registry rejected authentication"
			}
			return ImageAvailabilityStatusCheckFailed, fmt.Sprintf("workflow step %s concluded %s", step.GetName(), step.GetConclusion())
		}
	}
	return ImageAvailabilityStatusCheckFailed, fmt.Sprintf("workflow concluded %s", run.GetConclusion())
}

func (c *client) queryJobLogs(ctx context.Context, jobID int64) string {
	logURL, _, err := c.github.Actions.GetWorkflowJobLogs(ctx, c.owner, c.repo, jobID, false)
	if err != nil || logURL == nil {
		return ""
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, logURL.String(), nil)
	if err != nil {
		return ""
	}
	resp, err := c.downloadHTTPClient.Do(req)
	if err != nil {
		return ""
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return ""
	}
	return strings.ToLower(string(body))
}

func queryLogsLookNotFound(logs string) bool {
	for _, marker := range []string{"not found", "manifest unknown", "name unknown"} {
		if strings.Contains(logs, marker) {
			return true
		}
	}
	return false
}

func queryLogsLookAuthFailure(logs string) bool {
	for _, marker := range []string{"unauthorized", "authentication required", "no basic auth credentials", "denied", "forbidden"} {
		if strings.Contains(logs, marker) {
			return true
		}
	}
	return false
}

func (c *client) downloadQueryArtifact(ctx context.Context, runID int64) (*queryArtifact, error) {
	artifacts, _, err := c.github.Actions.ListWorkflowRunArtifacts(
		ctx,
		c.owner,
		c.repo,
		runID,
		&github.ListOptions{PerPage: 100},
	)
	if err != nil {
		return nil, fmt.Errorf("list image availability artifacts: %w", err)
	}
	var artifactID int64
	if artifacts != nil {
		for _, artifact := range artifacts.Artifacts {
			if artifact.GetName() == queryArtifactName && !artifact.GetExpired() {
				artifactID = artifact.GetID()
				break
			}
		}
	}
	if artifactID == 0 {
		return nil, fmt.Errorf("image availability workflow %d did not produce %s", runID, queryArtifactName)
	}
	downloadURL, _, err := c.github.Actions.DownloadArtifact(ctx, c.owner, c.repo, artifactID, false)
	if err != nil {
		return nil, fmt.Errorf("download image availability artifact: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL.String(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.downloadHTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("fetch image availability artifact: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("fetch image availability artifact: status %s", resp.Status)
	}
	archiveBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	archive, err := zip.NewReader(bytes.NewReader(archiveBytes), int64(len(archiveBytes)))
	if err != nil {
		return nil, fmt.Errorf("open image availability artifact: %w", err)
	}
	for _, file := range archive.File {
		if file.Name != queryArtifactName {
			continue
		}
		reader, err := file.Open()
		if err != nil {
			return nil, err
		}
		data, readErr := io.ReadAll(reader)
		closeErr := reader.Close()
		if readErr != nil {
			return nil, readErr
		}
		if closeErr != nil {
			return nil, closeErr
		}
		result := &queryArtifact{}
		if err := json.Unmarshal(data, result); err != nil {
			return nil, fmt.Errorf("decode image availability artifact: %w", err)
		}
		return result, nil
	}
	return nil, fmt.Errorf("%s is missing from image availability artifact", queryArtifactName)
}
