package read

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/jmrGrav/mcp-hugo-server-go/internal/config"
	"github.com/jmrGrav/mcp-hugo-server-go/internal/contentmodel"
	"github.com/jmrGrav/mcp-hugo-server-go/internal/hugosite"
	"github.com/jmrGrav/mcp-hugo-server-go/internal/site"
	"github.com/jmrGrav/mcp-hugo-server-go/internal/toolcontract"
	"github.com/jmrGrav/mcp-hugo-server-go/internal/tools/admin"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type listPageAssetsInput struct {
	Slug         string `json:"slug"`
	ResponseMode string `json:"response_mode,omitempty"`
}

// pageAssetDTO describes one sibling file stored alongside a page bundle's
// index.md (e.g. an image referenced by the page). #348: complements
// upload_page_asset (content.write) by letting agents discover what already
// exists in a bundle before writing a new asset.
type pageAssetDTO struct {
	Name       string `json:"name"`
	SizeBytes  int64  `json:"size_bytes"`
	ModifiedAt string `json:"modified_at"`
	// Sha256 is the same contentmodel.SourceRevisionBytes hash format
	// upload_page_asset/delete_page_asset use for expected_sha256 — this
	// tool's own description already promises it as the way to fetch that
	// value before calling delete_page_asset (#574).
	Sha256 string `json:"sha256"`
}

type generatedPageAssetDTO struct {
	Name       string `json:"name"`
	Path       string `json:"path"`
	Kind       string `json:"kind"`
	SizeBytes  int64  `json:"size_bytes"`
	ModifiedAt string `json:"modified_at"`
	Sha256     string `json:"sha256"`
}

type listPageAssetsData struct {
	Assets          []pageAssetDTO          `json:"assets"`
	GeneratedAssets []generatedPageAssetDTO `json:"generated_assets,omitempty"`
	// Hint is populated only when Assets is empty, clarifying that this
	// tool only lists page-bundle sibling files — a page can still
	// reference images under the site's global static directory that
	// won't show up here (#569). Additive only, never present alongside a
	// non-empty Assets list.
	Hint string `json:"hint,omitempty"`
}

type listPageAssetsOutput struct {
	toolcontract.ToolResponse[listPageAssetsData]
}

// emptyPageAssetsHint (#569) clarifies scope when data.assets is empty: this
// tool only lists sibling files in the page bundle's own directory, never
// the site's global static assets a page's rendered HTML may still
// reference (e.g. content under /images/...).
const emptyPageAssetsHint = "No page-bundle assets found. Rendered content may still reference global static assets."

func newListPageAssetsOutput(data listPageAssetsData, now time.Time) listPageAssetsOutput {
	return listPageAssetsOutput{ToolResponse: successEnvelope(data, now)}
}

// RegisterListPageAssets registers list_page_assets. Separate function
// (mirrors RegisterListContentTypes/RegisterInspectRenderedPage) since it
// needs its own PageResolver per call, not shared registration state.
func RegisterListPageAssets(s *mcp.Server, idx *site.Index, srcIdx *hugosite.SourceIndex, cfg config.Config) {
	if s == nil {
		return
	}
	addReadOnlyTool(s, "list_page_assets", "List page assets",
		"List the sibling files (images, etc.) stored alongside a Hugo page bundle's index.md, e.g. cover.webp next to content/posts/article/index.md. Only leaf page bundles have an asset directory; single-file pages (slug.md, no per-page directory) fail with not_a_bundle. Responses also surface generated hero images separately under data.generated_assets when {HugoRoot}/static/images/{slug}-featured.jpg exists, so agents can distinguish bundle-local files from globally generated static assets. Use before upload_page_asset to check what already exists, or before delete_page_asset to get the current sha256/revision for its expected_sha256/expected_revision concurrency guard. The on-disk bundle directory is a source-derived signal unavailable to the reader profile even when the page itself is public; readers receive an empty assets list instead of an error. Reader tool: on OAuth-enabled deployments, call it with a read Bearer token.",
		func(ctx context.Context, _ *mcp.CallToolRequest, in listPageAssetsInput) (*mcp.CallToolResult, listPageAssetsOutput, error) {
			slug := strings.Trim(strings.TrimSpace(in.Slug), "/")
			if slug == "" {
				return nil, listPageAssetsOutput{}, fmt.Errorf("invalid_params: slug must not be empty")
			}
			resolver := site.NewPageResolver(idx, srcIdx, cfg)
			resolved, ok := resolver.Resolve(slug)
			if !ok {
				return nil, listPageAssetsOutput{}, fmt.Errorf("content_not_found: page not found for slug %q", slug)
			}
			resolved, err := readerSafeResolvedPage(ctx, resolved, slug)
			if err != nil {
				return nil, listPageAssetsOutput{}, err
			}
			if resolved.SourcePath == "" {
				// Reader profile: the source bundle directory is intentionally
				// unavailable (see site.ReaderSafeResolvedPage), so there is no
				// path to list. Return an empty list rather than an error, same
				// omission-not-block treatment as #324/#339's source-derived
				// fields.
				return nil, newListPageAssetsOutput(listPageAssetsData{Assets: []pageAssetDTO{}, Hint: emptyPageAssetsHint}, time.Now().UTC()), nil
			}

			base := filepath.Base(resolved.SourcePath)
			if !strings.HasPrefix(base, "index.") {
				return nil, listPageAssetsOutput{}, fmt.Errorf("not_a_bundle: slug %q is a single-file page with no bundle directory for assets", slug)
			}
			dir := filepath.Dir(resolved.SourcePath)
			entries, err := os.ReadDir(dir)
			if err != nil {
				slog.Error("list_page_assets: read dir failed", "slug", slug, "error", err)
				return nil, listPageAssetsOutput{}, fmt.Errorf("read_error: failed to list page bundle directory")
			}

			assets := make([]pageAssetDTO, 0, len(entries))
			for _, e := range entries {
				if e.IsDir() {
					continue
				}
				name := e.Name()
				if strings.HasPrefix(name, "index.") && strings.HasSuffix(name, ".md") {
					continue
				}
				if strings.HasPrefix(name, ".") {
					continue
				}
				info, err := e.Info()
				if err != nil {
					continue
				}
				raw, err := os.ReadFile(filepath.Join(dir, name))
				if err != nil {
					slog.Warn("list_page_assets: read asset for sha256 failed", "slug", slug, "name", name, "error", err)
					continue
				}
				assets = append(assets, pageAssetDTO{
					Name:       name,
					SizeBytes:  info.Size(),
					ModifiedAt: info.ModTime().UTC().Format(time.RFC3339),
					Sha256:     contentmodel.SourceRevisionBytes(raw),
				})
			}
			sort.Slice(assets, func(i, j int) bool { return assets[i].Name < assets[j].Name })
			generatedAssets := listGeneratedHeroAssets(cfg.HugoRoot, slug)

			data := listPageAssetsData{Assets: assets, GeneratedAssets: generatedAssets}
			if len(assets) == 0 && len(generatedAssets) == 0 {
				data.Hint = emptyPageAssetsHint
			}
			return nil, newListPageAssetsOutput(data, time.Now().UTC()), nil
		})
}

func listGeneratedHeroAssets(hugoRoot, slug string) []generatedPageAssetDTO {
	if strings.TrimSpace(hugoRoot) == "" {
		return nil
	}
	loc, err := admin.ResolveHeroImageLocation(hugoRoot, slug)
	if err != nil {
		return nil
	}
	info, err := os.Stat(loc.AbsPath)
	if err != nil || info.IsDir() {
		return nil
	}
	raw, err := os.ReadFile(loc.AbsPath)
	if err != nil {
		slog.Warn("list_page_assets: read generated asset for sha256 failed", "slug", slug, "path", loc.LogicalPath, "error", err)
		return nil
	}
	return []generatedPageAssetDTO{{
		Name:       loc.Name,
		Path:       loc.LogicalPath,
		Kind:       "global_static",
		SizeBytes:  info.Size(),
		ModifiedAt: info.ModTime().UTC().Format(time.RFC3339),
		Sha256:     contentmodel.SourceRevisionBytes(raw),
	}}
}
