package cli

import (
	"encoding/json"
	"fmt"
	"io"
	"path/filepath"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

const runResultSchemaVersion = "notarius.run-result.v2"

type runResult struct {
	SchemaVersion             string                        `json:"schema_version"`
	RunID                     string                        `json:"run_id"`
	PipelineID                string                        `json:"pipeline_id"`
	OutputDirectory           string                        `json:"output_directory"`
	IndexFile                 string                        `json:"index_file,omitempty"`
	NormalizedOutputCount     int                           `json:"normalized_output_count"`
	RejectedOutputCount       int                           `json:"rejected_output_count"`
	WarningGroupCount         int                           `json:"warning_group_count"`
	WarningOccurrenceCount    int                           `json:"warning_occurrence_count"`
	DiagnosticGroupCount      int                           `json:"diagnostic_group_count"`
	DiagnosticOccurrenceCount int                           `json:"diagnostic_occurrence_count"`
	DiagnosticsTruncated      bool                          `json:"diagnostics_truncated"`
	ValidationStatus          string                        `json:"validation_status"`
	ValidationSummaries       []artifacts.ValidationSummary `json:"validation_summaries,omitempty"`
	DebugDirectory            string                        `json:"debug_directory,omitempty"`
}

func newRunResult(resolved pipeline.ResolvedPipeline, output pipeline.RunOutput, outputDirectory, debugDirectory string) (runResult, error) {
	diagnosticProjection, err := contracts.ProjectDiagnosticCollection(output.Diagnostics)
	if err != nil {
		return runResult{}, fmt.Errorf("summarize run diagnostics: %w", err)
	}
	return newRunResultWithDiagnostics(resolved, output, outputDirectory, debugDirectory, diagnosticProjection)
}

func newRunResultWithDiagnostics(resolved pipeline.ResolvedPipeline, output pipeline.RunOutput, outputDirectory, debugDirectory string, diagnosticProjection contracts.DiagnosticProjection) (runResult, error) {
	if strings.TrimSpace(output.Manifest.RunID) == "" {
		return runResult{}, fmt.Errorf("run result requires a run ID")
	}
	if strings.TrimSpace(resolved.ID) == "" {
		return runResult{}, fmt.Errorf("run result requires a resolved pipeline ID")
	}
	if strings.TrimSpace(output.Manifest.PipelineID) == "" {
		return runResult{}, fmt.Errorf("run result requires a manifest pipeline ID")
	}
	if output.Manifest.PipelineID != resolved.ID {
		return runResult{}, fmt.Errorf("run result pipeline ID does not match resolved pipeline")
	}
	if strings.TrimSpace(output.Manifest.ValidationStatus) == "" {
		return runResult{}, fmt.Errorf("run result requires a validation status")
	}
	if strings.TrimSpace(outputDirectory) == "" {
		return runResult{}, fmt.Errorf("run result requires an output directory")
	}

	absOutputDirectory, err := filepath.Abs(outputDirectory)
	if err != nil {
		return runResult{}, fmt.Errorf("make output directory absolute: %w", err)
	}

	result := runResult{
		SchemaVersion:             runResultSchemaVersion,
		RunID:                     output.Manifest.RunID,
		PipelineID:                output.Manifest.PipelineID,
		OutputDirectory:           absOutputDirectory,
		NormalizedOutputCount:     len(output.NormalizeOutputs),
		RejectedOutputCount:       len(output.Rejected),
		WarningGroupCount:         len(diagnosticProjection.Warnings),
		WarningOccurrenceCount:    diagnosticProjection.WarningOccurrenceCount,
		DiagnosticGroupCount:      len(diagnosticProjection.Diagnostics),
		DiagnosticOccurrenceCount: diagnosticProjection.DiagnosticOccurrenceCount,
		DiagnosticsTruncated:      output.Diagnostics.Truncated,
		ValidationStatus:          output.Manifest.ValidationStatus,
		ValidationSummaries:       cloneValidationSummaries(output.Manifest.ValidationSummaries),
	}

	if strings.TrimSpace(debugDirectory) != "" {
		absDebugDirectory, err := filepath.Abs(debugDirectory)
		if err != nil {
			return runResult{}, fmt.Errorf("make debug directory absolute: %w", err)
		}
		result.DebugDirectory = absDebugDirectory
	}

	if resolved.Output.Module == pipeline.DefaultOutputModule {
		indexCount := 0
		for _, file := range output.OutputFiles {
			if file.Name == "index.json" {
				indexCount++
			}
		}
		if indexCount != 1 {
			return runResult{}, fmt.Errorf("production JSON output must contain exactly one index.json file")
		}
		result.IndexFile = "index.json"
	}

	return result, nil
}

func cloneValidationSummaries(summaries []artifacts.ValidationSummary) []artifacts.ValidationSummary {
	if len(summaries) == 0 {
		return nil
	}
	cloned := make([]artifacts.ValidationSummary, len(summaries))
	for index, summary := range summaries {
		cloned[index] = artifacts.CloneValidationSummary(summary)
	}
	return cloned
}

func encodeRunResult(result runResult) ([]byte, error) {
	encoded, err := json.Marshal(result)
	if err != nil {
		return nil, fmt.Errorf("encode run result: %w", err)
	}
	return append(encoded, '\n'), nil
}

func writeRunResult(writer io.Writer, content []byte) error {
	for len(content) > 0 {
		written, err := writer.Write(content)
		if written < 0 || written > len(content) {
			return io.ErrShortWrite
		}
		content = content[written:]
		if err != nil {
			return err
		}
		if written == 0 {
			return io.ErrShortWrite
		}
	}
	return nil
}
