package bunny

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"tsne.dev/hopper/internal/log"
)

// Iterator is an old-school pull iterator over the recursive remote listing under
// a starting directory. It yields every entry (files AND directories; a
// directory has Size 0) so consumers can build whatever structure they need:
// push collects files and verifies no file collides with a remote directory;
// prune derives orphans and empty directories. Usage:
//
//	it := client.List(ctx, dir)
//	for it.Next() {
//		rec := it.Object()
//	}
//	if err := it.Err(); err != nil { ... }
//
// The walk is iterative: a stack holds directories still to list, each listed
// on demand. A 404 listing is an empty subtree (skipped, not fatal); any other
// listing failure stops iteration and surfaces via Err.
type Iterator struct {
	ctx     context.Context
	listDir func(context.Context, string) ([]Object, error)
	stack   []string
	pending []Object
	cur     Object
	err     error
	done    bool
}

// NewIterator builds an Iterator rooted at dir, listing each directory via listDir.
func NewIterator(ctx context.Context, dir string, listDir func(context.Context, string) ([]Object, error)) *Iterator {
	stack := make([]string, 0, 4)
	stack = append(stack, dir)
	return &Iterator{ctx: ctx, listDir: listDir, stack: stack}
}

// Next advances to the next entry, returning false when the walk is exhausted
// or a listing failed (check Err). Directory entries are yielded and also queued
// for descent.
func (it *Iterator) Next() bool {
	if it.err != nil || it.done {
		return false
	}
	for {
		if len(it.pending) > 0 {
			o := it.pending[0]
			it.pending = it.pending[1:]
			if o.IsDirectory {
				it.stack = append(it.stack, o.Path)
			}
			it.cur = o
			return true
		}
		if len(it.stack) == 0 {
			it.done = true
			return false
		}
		dir := it.stack[len(it.stack)-1]
		it.stack = it.stack[:len(it.stack)-1]
		objs, err := it.listDir(it.ctx, dir)
		if err != nil {
			var apiErr *apiError
			if errors.As(err, &apiErr) && apiErr.status == http.StatusNotFound {
				continue // 404 = empty subtree
			}
			it.err = err
			return false
		}
		it.pending = objs
	}
}

// Object returns the entry reached by the most recent Next.
func (it *Iterator) Object() Object {
	return it.cur
}

// Err returns the listing failure that stopped iteration, if any.
func (it *Iterator) Err() error {
	return it.err
}

// List returns an Iterator over the recursive listing rooted at dir. An empty
// dir denotes zone root.
func (api *API) List(ctx context.Context, dir string) *Iterator {
	return NewIterator(ctx, dir, api.listDir)
}

// listDir returns the immediate children of dir. Bunny's list endpoint is
// per-directory and expects a trailing slash on the directory URL. Returned
// Object.Path is the full storage key relative to the zone (matching source
// keys), so the skip-check can match it.
func (api *API) listDir(ctx context.Context, dir string) ([]Object, error) {
	if dir == "" {
		log.PrintVerbose("listing zone root")
	} else {
		log.PrintVerbose("listing %s", dir)
	}

	u := api.url(dir)
	if !strings.HasSuffix(u, "/") {
		u += "/"
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}

	resp, err := api.do(ctx, req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var raw []listObject
	if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
		return nil, newAPIError(errorKindInternal, errors.New("failed to decode list response ("+err.Error()+")"), req, resp.StatusCode)
	}

	prefix := "/" + api.zone + "/"
	objs := make([]Object, len(raw))
	for i, o := range raw {
		objs[i] = Object{
			Path:        strings.TrimPrefix(o.Path, prefix) + o.ObjectName,
			Size:        o.Length,
			IsDirectory: o.IsDirectory,
			LastChanged: parseTime(o.LastChanged),
		}
	}
	return objs, nil
}

// listObject mirrors a single entry of Bunny's per-directory list response. The
// full storage key is Path+ObjectName (Path includes the zone and trailing
// slash); see List.
type listObject struct {
	Path        string `json:"Path"`
	ObjectName  string `json:"ObjectName"`
	Length      int    `json:"Length"`
	LastChanged string `json:"LastChanged"`
	IsDirectory bool   `json:"IsDirectory"`
}
