package imagetools

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"
	"text/tabwriter"
	"text/template"

	"github.com/containerd/containerd/images"
	"github.com/containerd/containerd/platforms"
	"github.com/docker/distribution/reference"
	"github.com/opencontainers/go-digest"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)

const defaultPfx = "  "

type Printer struct {
	ctx      context.Context
	resolver *Resolver

	name   string
	format string

	raw      []byte
	ref      reference.Named
	manifest ocispecs.Descriptor
	index    ocispecs.Index
}

func NewPrinter(ctx context.Context, opt Opt, name string, format string) (*Printer, error) {
	resolver := New(opt)

	ref, err := parseRef(name)
	if err != nil {
		return nil, err
	}

	dt, mfst, err := resolver.Get(ctx, ref.String())
	if err != nil {
		return nil, err
	}

	var idx ocispecs.Index
	if err = json.Unmarshal(dt, &idx); err != nil {
		return nil, err
	}

	return &Printer{
		ctx:      ctx,
		resolver: resolver,
		name:     name,
		format:   format,
		raw:      dt,
		ref:      ref,
		manifest: mfst,
		index:    idx,
	}, nil
}

func (p *Printer) Print(raw bool, out io.Writer) error {
	if raw {
		_, err := fmt.Fprintf(out, "%s", p.raw) // avoid newline to keep digest
		return err
	}

	if p.format == "" {
		w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
		_, _ = fmt.Fprintf(w, "Name:\t%s\n", p.ref.String())
		_, _ = fmt.Fprintf(w, "MediaType:\t%s\n", p.manifest.MediaType)
		_, _ = fmt.Fprintf(w, "Digest:\t%s\n", p.manifest.Digest)
		_ = w.Flush()
		switch p.manifest.MediaType {
		case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex:
			if err := p.printManifestList(out); err != nil {
				return err
			}
		}
		return nil
	}

	res, err := newLoader(p.resolver.resolver()).Load(p.ctx, p.name)
	if err != nil {
		return err
	}

	tpl, err := template.New("").Funcs(template.FuncMap{
		"json": func(v interface{}) string {
			b, _ := json.MarshalIndent(v, "", "  ")
			return string(b)
		},
	}).Parse(p.format)
	if err != nil {
		return err
	}

	imageconfigs := res.Configs()
	format := tpl.Root.String()

	var mfst interface{}
	switch p.manifest.MediaType {
	case images.MediaTypeDockerSchema2Manifest, ocispecs.MediaTypeImageManifest:
		mfst = p.manifest
	case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex:
		mfst = struct {
			SchemaVersion int                   `json:"schemaVersion"`
			MediaType     string                `json:"mediaType,omitempty"`
			Digest        digest.Digest         `json:"digest"`
			Size          int64                 `json:"size"`
			Manifests     []ocispecs.Descriptor `json:"manifests"`
			Annotations   map[string]string     `json:"annotations,omitempty"`
		}{
			SchemaVersion: p.index.Versioned.SchemaVersion,
			MediaType:     p.index.MediaType,
			Digest:        p.manifest.Digest,
			Size:          p.manifest.Size,
			Manifests:     p.index.Manifests,
			Annotations:   p.index.Annotations,
		}
	}

	switch {
	// TODO: print formatted config
	case strings.HasPrefix(format, "{{.Manifest"):
		w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
		_, _ = fmt.Fprintf(w, "Name:\t%s\n", p.ref.String())
		switch {
		case strings.HasPrefix(format, "{{.Manifest"):
			_, _ = fmt.Fprintf(w, "MediaType:\t%s\n", p.manifest.MediaType)
			_, _ = fmt.Fprintf(w, "Digest:\t%s\n", p.manifest.Digest)
			_ = w.Flush()
			switch p.manifest.MediaType {
			case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex:
				_ = p.printManifestList(out)
			}
		}
	default:
		if len(res.platforms) > 1 {
			return tpl.Execute(out, tplInputs{
				Name:     p.name,
				Manifest: mfst,
				Image:    imageconfigs,
				result:   res,
			})
		}
		var ic *ocispecs.Image
		for _, v := range imageconfigs {
			ic = v
		}
		return tpl.Execute(out, tplInput{
			Name:     p.name,
			Manifest: mfst,
			Image:    ic,
			result:   res,
		})
	}

	return nil
}

func (p *Printer) printManifestList(out io.Writer) error {
	w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
	_, _ = fmt.Fprintf(w, "\t\n")
	_, _ = fmt.Fprintf(w, "Manifests:\t\n")
	_ = w.Flush()

	w = tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
	for i, m := range p.index.Manifests {
		if i != 0 {
			_, _ = fmt.Fprintf(w, "\t\n")
		}
		cr, err := reference.WithDigest(p.ref, m.Digest)
		if err != nil {
			return err
		}
		_, _ = fmt.Fprintf(w, "%sName:\t%s\n", defaultPfx, cr.String())
		_, _ = fmt.Fprintf(w, "%sMediaType:\t%s\n", defaultPfx, m.MediaType)
		if p := m.Platform; p != nil {
			_, _ = fmt.Fprintf(w, "%sPlatform:\t%s\n", defaultPfx, platforms.Format(*p))
			if p.OSVersion != "" {
				_, _ = fmt.Fprintf(w, "%sOSVersion:\t%s\n", defaultPfx, p.OSVersion)
			}
			if len(p.OSFeatures) > 0 {
				_, _ = fmt.Fprintf(w, "%sOSFeatures:\t%s\n", defaultPfx, strings.Join(p.OSFeatures, ", "))
			}
			if len(m.URLs) > 0 {
				_, _ = fmt.Fprintf(w, "%sURLs:\t%s\n", defaultPfx, strings.Join(m.URLs, ", "))
			}
			if len(m.Annotations) > 0 {
				_, _ = fmt.Fprintf(w, "%sAnnotations:\t\n", defaultPfx)
				_ = w.Flush()
				w2 := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
				for k, v := range m.Annotations {
					_, _ = fmt.Fprintf(w2, "%s%s:\t%s\n", defaultPfx+defaultPfx, k, v)
				}
				_ = w2.Flush()
			}
		}
	}
	return w.Flush()
}

type tplInput struct {
	Name     string          `json:"name,omitempty"`
	Manifest interface{}     `json:"manifest,omitempty"`
	Image    *ocispecs.Image `json:"image,omitempty"`

	result *result
}

func (inp tplInput) SBOM() (sbomStub, error) {
	sbom, err := inp.result.SBOM()
	if err != nil {
		return sbomStub{}, nil
	}
	for _, v := range sbom {
		return v, nil
	}
	return sbomStub{}, nil
}

func (inp tplInput) Provenance() (provenanceStub, error) {
	provenance, err := inp.result.Provenance()
	if err != nil {
		return provenanceStub{}, nil
	}
	for _, v := range provenance {
		return v, nil
	}
	return provenanceStub{}, nil
}

type tplInputs struct {
	Name     string                     `json:"name,omitempty"`
	Manifest interface{}                `json:"manifest,omitempty"`
	Image    map[string]*ocispecs.Image `json:"image,omitempty"`

	result *result
}

func (inp tplInputs) SBOM() (map[string]sbomStub, error) {
	return inp.result.SBOM()
}

func (inp tplInputs) Provenance() (map[string]provenanceStub, error) {
	return inp.result.Provenance()
}