package rosa

import (
	"compress/gzip"
	"context"
	"crypto/ed25519"
	"crypto/sha512"
	"errors"
	"io"
	"io/fs"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"unique"

	"hakurei.app/internal/pkg"
	"hakurei.app/message"
)

// Remote is an authenticated cache mirror.
type Remote struct {
	// Mirror URL.
	url *url.URL
	// Trusted public key.
	pub ed25519.PublicKey
	// For requests to the mirror.
	c *http.Client
}

// NewRemote returns a populated [Remote].
func NewRemote(c *http.Client, base string, pub ed25519.PublicKey) (Remote, error) {
	u, err := url.Parse(base)
	return Remote{u, pub, c}, err
}

// get makes a [http.MethodGet] request and returns the response, or nil if
// the response StatusCode is [http.StatusNotFound].
func (r Remote) get(ctx context.Context, elem ...string) (*http.Response, error) {
	if r.url == nil || len(r.pub) != ed25519.PublicKeySize || r.c == nil {
		return nil, os.ErrInvalid
	}

	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		r.url.JoinPath(elem...).String(),
		nil,
	)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "Rosa/1.1")

	var resp *http.Response
	if resp, err = r.c.Do(req); err != nil {
		return nil, err
	}

	switch resp.StatusCode {
	case http.StatusOK:
		return resp, nil

	case http.StatusNotFound:
		return nil, resp.Body.Close()

	default:
		_ = resp.Body.Close()
		return nil, pkg.ResponseStatusError(resp.StatusCode)
	}
}

const (
	// dirArtifact holds signed artifact outcome checksums.
	dirArtifact = "artifact"
	// dirOutcome holds outcome archives by their checksum.
	dirOutcome = "outcome"
	// dirStatus holds signed status files.
	dirStatus = "status"
)

// An OutcomeBadSizeError describes a mirror outcome with unexpected size.
type OutcomeBadSizeError struct {
	Ident unique.Handle[pkg.ID]
	Size  int64
}

func (e OutcomeBadSizeError) Error() string {
	if e.Size < 0 {
		return "remote did not return outcome size for " +
			pkg.Encode(e.Ident.Value())
	}
	return "outcome size " + strconv.FormatInt(e.Size, 10) +
		" invalid for " + pkg.Encode(e.Ident.Value())
}

// An OutcomeAuthError describes a mirror outcome with invalid signature.
type OutcomeAuthError unique.Handle[pkg.ID]

func (e OutcomeAuthError) Error() string {
	return "invalid outcome signature for " +
		pkg.Encode(unique.Handle[pkg.ID](e).Value())
}

// Artifact fetches and authenticates an outcome.
func (r Remote) Artifact(
	ctx context.Context,
	id unique.Handle[pkg.ID],
) (*pkg.Checksum, error) {
	if len(r.pub) != ed25519.PublicKeySize || r.c == nil {
		return nil, os.ErrInvalid
	}

	resp, err := r.get(ctx, dirArtifact, pkg.Encode(id.Value()))
	if err != nil || resp == nil {
		return nil, err
	}

	var buf [ed25519.SignatureSize + 2*len(pkg.Checksum{})]byte
	if resp.ContentLength != int64(len(buf)) {
		_ = resp.Body.Close()
		return nil, OutcomeBadSizeError{id, resp.ContentLength}
	}
	if _, err = io.ReadFull(resp.Body, buf[:]); err != nil {
		return nil, errors.Join(err, resp.Body.Close())
	} else if err = resp.Body.Close(); err != nil {
		return nil, err
	}

	if !ed25519.Verify(
		r.pub,
		buf[ed25519.SignatureSize:],
		buf[:ed25519.SignatureSize],
	) {
		return nil, OutcomeAuthError(id)
	} else if unique.Make((pkg.ID)(buf[ed25519.SignatureSize:])) != id {
		return nil, OutcomeAuthError(id)
	}
	return (*pkg.Checksum)(buf[ed25519.SignatureSize+len(pkg.Checksum{}):]), nil
}

// Checksum returns an artifact satisfying checksum.
func (r Remote) Checksum(checksum unique.Handle[pkg.Checksum]) pkg.Artifact {
	return pkg.NewArchive(pkg.NewHTTPGet(
		r.c,
		r.url.JoinPath(dirOutcome, pkg.Encode(checksum.Value())).String(),
		checksum.Value(),
	))
}

// A StatusBadSizeError describes a mirror status with unexpected size.
type StatusBadSizeError unique.Handle[pkg.ID]

func (e StatusBadSizeError) Error() string {
	return "status payload too short for " +
		pkg.Encode(unique.Handle[pkg.ID](e).Value())
}

// A StatusAuthError describes a mirror status with invalid signature.
type StatusAuthError unique.Handle[pkg.ID]

func (e StatusAuthError) Error() string {
	return "invalid status signature for " +
		pkg.Encode(unique.Handle[pkg.ID](e).Value())
}

// Status authenticates the checksum of a status file and returns its
// corresponding measured reader.
func (r Remote) Status(
	ctx *pkg.RContext,
	id unique.Handle[pkg.ID],
) (io.ReadCloser, error) {
	resp, err := r.get(ctx.Unwrap(), dirStatus, pkg.Encode(id.Value()))
	if err != nil || resp == nil {
		return nil, err
	}

	var buf [ed25519.SignatureSize + 2*len(pkg.Checksum{})]byte
	if _, err = io.ReadFull(resp.Body, buf[:]); err != nil {
		if errors.Is(err, io.ErrUnexpectedEOF) {
			err = StatusBadSizeError(id)
		}
		return nil, err
	}

	if !ed25519.Verify(
		r.pub,
		buf[ed25519.SignatureSize:],
		buf[:ed25519.SignatureSize],
	) {
		_ = resp.Body.Close()
		return nil, StatusAuthError(id)
	} else if unique.Make((pkg.ID)(buf[ed25519.SignatureSize:])) != id {
		return nil, StatusAuthError(id)
	}
	return ctx.NewMeasuredReader(
		resp.Body,
		unique.Make((pkg.Checksum)(buf[ed25519.SignatureSize+len(pkg.Checksum{}):])),
	), nil
}

// Mirror services requests made by [Remote].
type Mirror struct {
	// Cache on-filesystem format.
	fsys fs.FS
	// Signs payloads.
	key ed25519.PrivateKey
	// Verbose output.
	msg message.Msg
}

// Populate fills an empty root with a static mirror snapshot.
func (m Mirror) Populate(root *os.Root) error {
	if dents, err := fs.ReadDir(root.FS(), "."); err != nil {
		return err
	} else if len(dents) > 0 {
		return syscall.ENOTEMPTY
	}

	for _, name := range []string{
		dirArtifact,
		dirOutcome,
		dirStatus,
	} {
		if err := root.Mkdir(name, 0755); err != nil {
			return err
		}
	}
	var w *os.File

	if dents, err := fs.ReadDir(m.fsys, "identifier"); err != nil {
		return err
	} else {
		for _, dent := range dents {
			name := dent.Name()
			var buf [2 * len(pkg.Checksum{})]byte
			if err = pkg.Decode(
				(*pkg.Checksum)(buf[:len(pkg.Checksum{})]),
				name,
			); err != nil {
				return err
			}

			if w, err = root.OpenFile(
				filepath.Join(dirArtifact, name),
				syscall.O_WRONLY|syscall.O_CREAT|syscall.O_EXCL,
				0444,
			); err != nil {
				return err
			}
			err = m.writeArtifact(&buf, w, nil)
			if err = errors.Join(err, w.Close()); err != nil {
				return err
			}
		}
	}

	if dents, err := fs.ReadDir(m.fsys, "checksum"); err != nil {
		return err
	} else {
		for _, dent := range dents {
			name := dent.Name()
			var buf pkg.Checksum
			if err = pkg.Decode(&buf, name); err != nil {
				return err
			}

			var fsys fs.FS
			fsys, err = fs.Sub(m.fsys, filepath.Join("checksum", name))
			if err != nil {
				return err
			}

			if w, err = root.OpenFile(
				filepath.Join(dirOutcome, name),
				syscall.O_WRONLY|syscall.O_CREAT|syscall.O_EXCL,
				0444,
			); err != nil {
				return err
			}

			m.msg.Verbosef("exporting outcome %s", name)
			err = pkg.Write(fsys, ".", w)
			if err = errors.Join(err, w.Close()); err != nil {
				return err
			}
		}
	}

	if dents, err := fs.ReadDir(m.fsys, "status"); err != nil {
		return err
	} else {
		for _, dent := range dents {
			name := dent.Name()
			var buf [2 * len(pkg.Checksum{})]byte
			if err = pkg.Decode(
				(*pkg.Checksum)(buf[:len(pkg.Checksum{})]),
				name,
			); err != nil {
				return err
			}

			if w, err = root.OpenFile(
				filepath.Join(dirStatus, name),
				syscall.O_WRONLY|syscall.O_CREAT|syscall.O_EXCL,
				0444,
			); err != nil {
				return err
			}
			err = m.writeStatus(&buf, w, nil)
			if err = errors.Join(err, w.Close()); err != nil {
				return err
			}
		}
	}

	return nil
}

// NewMirror returns a populated [Mirror] for servicing mirror requests.
func NewMirror(
	msg message.Msg,
	fsys fs.FS,
	key ed25519.PrivateKey,
) Mirror {
	return Mirror{fsys, key, msg}
}

// writeArtifact writes a signed dirArtifact response.
func (m Mirror) writeArtifact(
	buf *[2 * len(pkg.Checksum{})]byte,
	w io.Writer,
	header http.Header,
) error {
	ids := pkg.Encode((pkg.Checksum)(buf[:len(pkg.Checksum{})]))
	if linkname, err := fs.ReadLink(m.fsys, filepath.Join(
		"identifier",
		ids,
	)); err != nil {
		return err
	} else if err = pkg.Decode(
		(*pkg.Checksum)(buf[len(pkg.Checksum{}):]),
		filepath.Base(linkname),
	); err != nil {
		return err
	}
	m.msg.Verbosef("serving artifact %s", ids)

	if header != nil {
		header.Set(
			"Content-Length",
			strconv.Itoa(ed25519.SignatureSize+len(buf)),
		)
	}
	_, err := w.Write(append(ed25519.Sign(m.key, buf[:]), buf[:]...))
	return err
}

// writeOutcome writes a dirOutcome response.
func (m Mirror) writeOutcome(
	buf *pkg.Checksum,
	w io.Writer,
	header http.Header,
) error {
	checksums := pkg.Encode(*buf)
	rel := filepath.Join("checksum", checksums)
	if _, err := fs.Lstat(m.fsys, rel); err != nil {
		return err
	}
	m.msg.Verbosef("serving outcome %s", checksums)

	_fsys, err := fs.Sub(m.fsys, rel)
	if err != nil {
		return err
	}

	var gw *gzip.Writer
	if gw, err = gzip.NewWriterLevel(w, gzip.BestCompression); err != nil {
		return err
	}
	if header != nil {
		header.Set("Content-Encoding", "gzip")
	}
	if err = pkg.Write(_fsys, ".", gw); err != nil {
		_ = gw.Close()
		return err
	}
	return gw.Close()
}

// writeStatus writes a signed dirStatus response.
func (m Mirror) writeStatus(
	buf *[2 * len(pkg.Checksum{})]byte,
	w io.Writer,
	_ http.Header,
) error {
	ids := pkg.Encode((pkg.Checksum)(buf[:len(pkg.Checksum{})]))
	f, err := m.fsys.Open(filepath.Join(
		"status",
		ids,
	))
	if err != nil {
		return err
	}
	s, ok := f.(io.Seeker)
	if !ok {
		return &fs.PathError{Op: "seek", Path: ids, Err: fs.ErrInvalid}
	}
	m.msg.Verbosef("serving status %s", ids)

	h := sha512.New384()
	if _, err = io.Copy(h, f); err != nil {
		_ = f.Close()
		return err
	}
	h.Sum(buf[len(pkg.Checksum{}):len(pkg.Checksum{})])
	if _, err = w.Write(append(ed25519.Sign(m.key, buf[:]), buf[:]...)); err != nil {
		_ = f.Close()
		return err
	} else if _, err = s.Seek(0, io.SeekStart); err != nil {
		_ = f.Close()
		return err
	} else if _, err = io.Copy(w, f); err != nil {
		_ = f.Close()
		return err
	}
	return f.Close()
}

// writeF is a helper for writing a mirror response with error handling.
func writeF[B any](
	m Mirror,
	w http.ResponseWriter,
	buf B,
	f func(
		buf B,
		w io.Writer,
		header http.Header,
	) error,
) {
	err := f(buf, w, w.Header())
	if err == nil {
		return
	}

	if errors.Is(err, os.ErrNotExist) {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	m.msg.GetLogger().Println(err)
	w.WriteHeader(http.StatusInternalServerError)
}

// Register configures an [http.ServeMux] for servicing mirror requests.
func (m Mirror) Register(mux *http.ServeMux) {
	const identName = "ident"

	mux.HandleFunc("/"+dirArtifact+"/{"+identName+"}", func(
		w http.ResponseWriter,
		req *http.Request,
	) {
		var buf [2 * len(pkg.Checksum{})]byte
		if err := pkg.Decode(
			(*pkg.Checksum)(buf[:len(pkg.Checksum{})]),
			req.PathValue(identName),
		); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		writeF(m, w, &buf, m.writeArtifact)
	})

	mux.HandleFunc("/"+dirOutcome+"/{"+identName+"}", func(
		w http.ResponseWriter,
		req *http.Request,
	) {
		if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
			w.WriteHeader(http.StatusNotAcceptable)
			return
		}

		var buf pkg.Checksum
		if err := pkg.Decode(
			&buf,
			req.PathValue(identName),
		); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		writeF(m, w, &buf, m.writeOutcome)
	})

	mux.HandleFunc("/"+dirStatus+"/{"+identName+"}", func(
		w http.ResponseWriter,
		req *http.Request,
	) {
		var buf [2 * len(pkg.Checksum{})]byte
		if err := pkg.Decode(
			(*pkg.Checksum)(buf[:len(pkg.Checksum{})]),
			req.PathValue(identName),
		); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		writeF(m, w, &buf, m.writeStatus)
	})
}
