package executor

import (
	"context"
	"encoding/json"

	"webby-builder/internal/browser"
	"webby-builder/internal/models"
	"webby-builder/internal/scrape"
	"webby-builder/internal/security/urlguard"
)

// SessionState aggregates the per-build mutable state the WebExecutor needs
// for the Firecrawl tier (quota counter, per-session cap, cache).
// In production the runner constructs this from session struct fields;
// in tests it is constructed directly.
type SessionState struct {
	FirecrawlRemaining *int                               // nil = unlimited, 0 = exhausted, N > 0 = N pages left
	FirecrawlPagesUsed int                                // total pages consumed in this session (for reconciliation)
	FirecrawlCallCount int                                // total Firecrawl tool invocations this session (for cap)
	FirecrawlCache     map[string]*scrape.FirecrawlResult // URL → result; no TTL within session
}

// WebExecutor handles all webby-plugin-webagent tools.
// Construct with NewWebExecutor and call Handle on tool calls whose Name
// matches IsWebTool.
type WebExecutor struct {
	HTTP      *scrape.Client
	Browser   *browser.Manager
	Caps      *models.WebAgentCapability
	firecrawl *scrape.FirecrawlClient // nil when not configured
	// sessionState is the live Firecrawl state for the current build session.
	// Set by the runner via SetSessionState before the agent loop starts.
	sessionState *SessionState
}

// NewWebExecutor builds a WebExecutor. Pass nil caps when the plugin is disabled.
func NewWebExecutor(httpClient *scrape.Client, mgr *browser.Manager, caps *models.WebAgentCapability) *WebExecutor {
	return &WebExecutor{HTTP: httpClient, Browser: mgr, Caps: caps}
}

// SetFirecrawlClient wires a FirecrawlClient into the executor.
// Called by the runner after constructing the executor when FirecrawlEnabled is true.
func (e *WebExecutor) SetFirecrawlClient(c *scrape.FirecrawlClient) {
	e.firecrawl = c
}

// SetSessionState wires the per-build Firecrawl session state into the executor
// so that webFetchFirecrawl calls can update quota counters and the cache in-place.
// Called by the runner after initialising session.firecrawl* fields.
func (e *WebExecutor) SetSessionState(state *SessionState) {
	e.sessionState = state
}

// IsWebTool reports whether a tool name is handled by this executor.
func IsWebTool(name string) bool {
	switch name {
	case "webFetchHttp", "webBrowserOpen", "webBrowserClick", "webBrowserType",
		"webBrowserScroll", "webBrowserFillForm", "webBrowserWaitFor",
		"webBrowserNavigate", "webBrowserGetDom", "webBrowserClose",
		"webFetchFirecrawl":
		return true
	}
	return false
}

// Handle dispatches a web tool call. buildSessionID is the agent's session ID
// (required to enforce per-build-session browser cap). ctx is the request
// context forwarded from the executor (used by Firecrawl HTTP calls).
// Returns the JSON-encoded result string the agent receives as the tool result.
func (e *WebExecutor) Handle(ctx context.Context, buildSessionID, toolName string, args map[string]any) string {
	if e.Caps == nil || !e.Caps.Enabled {
		return errResult("not_enabled", "the web agent plugin is not enabled for this project", false, "")
	}
	switch toolName {
	case "webFetchHttp":
		return e.fetchHTTP(args)
	case "webBrowserOpen":
		return e.browserOpen(buildSessionID, args)
	case "webBrowserClick":
		return e.browserAction(args, func(s *browser.Session) error { return s.Click(strArg(args, "selector")) })
	case "webBrowserType":
		return e.browserAction(args, func(s *browser.Session) error {
			return s.Type(strArg(args, "selector"), strArg(args, "text"))
		})
	case "webBrowserScroll":
		return e.browserAction(args, func(s *browser.Session) error {
			return s.Scroll(strArg(args, "direction"), intArg(args, "amount_px"))
		})
	case "webBrowserFillForm":
		return e.browserAction(args, func(s *browser.Session) error {
			fields, _ := args["fields"].([]any)
			ff := make([]browser.FormField, 0, len(fields))
			for _, f := range fields {
				if m, ok := f.(map[string]any); ok {
					ff = append(ff, browser.FormField{
						Selector: strFrom(m, "selector"),
						Value:    strFrom(m, "value"),
					})
				}
			}
			return s.FillForm(ff)
		})
	case "webBrowserWaitFor":
		return e.browserAction(args, func(s *browser.Session) error {
			return s.WaitFor(strArg(args, "selector"), intArg(args, "ms"))
		})
	case "webBrowserNavigate":
		return e.browserAction(args, func(s *browser.Session) error {
			return s.NavigateHistory(strArg(args, "direction"))
		})
	case "webBrowserGetDom":
		return e.browserGetDom(args)
	case "webBrowserClose":
		return e.browserClose(args)
	case "webFetchFirecrawl":
		state := e.sessionState
		if state == nil {
			// sessionState is nil when the runner didn't wire Firecrawl (e.g. the
			// feature flag is off). Return a clear error rather than panicking.
			return firecrawlErrorJSON("firecrawl_unavailable", "firecrawl session state not initialised", false)
		}
		result, _ := e.HandleFirecrawl(ctx, state, args)
		return result
	}
	return errResult("unknown_tool", "unknown web tool: "+toolName, false, "")
}

func (e *WebExecutor) fetchHTTP(args map[string]any) string {
	url := strArg(args, "url")
	res, err := e.HTTP.Fetch(url)
	if err != nil {
		return fetchErrToJSON(err)
	}
	b, _ := json.Marshal(res)
	return string(b)
}

func (e *WebExecutor) browserOpen(buildSessionID string, args map[string]any) string {
	s, err := e.Browser.Open(buildSessionID, strArg(args, "url"))
	if err != nil {
		return managerErrToJSON(err)
	}
	ps, _ := s.BuildPageState(0)
	out := map[string]any{
		"session_id": s.ID,
		"page_state": ps,
	}
	b, _ := json.Marshal(out)
	return string(b)
}

func (e *WebExecutor) browserAction(args map[string]any, do func(*browser.Session) error) string {
	id := strArg(args, "session_id")
	s, err := e.Browser.Get(id)
	if err != nil {
		return managerErrToJSON(err)
	}
	if err := do(s); err != nil {
		return managerErrToJSON(err)
	}
	ps, _ := s.BuildPageState(0)
	b, _ := json.Marshal(map[string]any{"page_state": ps})
	return string(b)
}

func (e *WebExecutor) browserGetDom(args map[string]any) string {
	id := strArg(args, "session_id")
	s, err := e.Browser.Get(id)
	if err != nil {
		return managerErrToJSON(err)
	}
	html, err := s.GetHTML()
	if err != nil {
		return managerErrToJSON(err)
	}
	r := scrape.ExtractFromHTML(html)
	b, _ := json.Marshal(r)
	return string(b)
}

func (e *WebExecutor) browserClose(args map[string]any) string {
	if err := e.Browser.Close(strArg(args, "session_id")); err != nil {
		return managerErrToJSON(err)
	}
	return `{"ok":true}`
}

// --- Firecrawl handler ---

// HandleFirecrawl handles a webFetchFirecrawl tool call. Returns the JSON-string
// result the agent receives in its tool_result event.
//
// The sequence:
//  1. Validate URL arg present.
//  2. Check session-level cache (URL → cached result); hit returns without
//     decrementing quota.
//  3. Check quota counter (nil = unlimited, 0 = exhausted → return error).
//  4. Check per-session cap.
//  5. Run urlguard.Validate on the URL (SSRF defense reused).
//  6. Call FirecrawlClient.Scrape.
//  7. On success: decrement counter if non-nil, bump pages_used, store in cache.
//  8. On failure: counter untouched (Firecrawl doesn't bill failed scrapes).
func (e *WebExecutor) HandleFirecrawl(ctx context.Context, state *SessionState, args map[string]any) (string, error) {
	url, _ := args["url"].(string)
	if url == "" {
		return firecrawlErrorJSON("firecrawl_bad_request", "url is required", false), nil
	}
	if e.firecrawl == nil {
		return firecrawlErrorJSON("firecrawl_unavailable", "firecrawl client not configured", false), nil
	}

	// 1. Cache hit — does not consume quota
	if state.FirecrawlCache != nil {
		if cached, ok := state.FirecrawlCache[url]; ok {
			return marshalFirecrawlResult(cached), nil
		}
	} else {
		state.FirecrawlCache = make(map[string]*scrape.FirecrawlResult)
	}

	// 2. Quota check (nil = unlimited, 0 = exhausted)
	if state.FirecrawlRemaining != nil && *state.FirecrawlRemaining <= 0 {
		return firecrawlErrorJSON("firecrawl_quota_exhausted", "monthly Firecrawl page quota exhausted", false), nil
	}

	// 3. Per-session cap
	maxCalls := 20
	if e.Caps != nil && e.Caps.FirecrawlMaxCallsPerSession > 0 {
		maxCalls = e.Caps.FirecrawlMaxCallsPerSession
	}
	if state.FirecrawlCallCount >= maxCalls {
		return firecrawlErrorJSON("firecrawl_session_cap_reached", "Firecrawl call cap reached for this build", false), nil
	}

	// 4. SSRF defense — same guard as the HTTP and browser tiers
	if err := urlguard.Validate(url); err != nil {
		return firecrawlErrorJSON("ssrf_blocked", err.Error(), false), nil
	}

	// 5. Call Firecrawl
	res, err := e.firecrawl.Scrape(ctx, url)
	if err != nil {
		if fe, ok := err.(*scrape.FirecrawlError); ok {
			return firecrawlErrorJSON(fe.Code, fe.Message, fe.Retryable), nil
		}
		return firecrawlErrorJSON("firecrawl_internal", err.Error(), false), nil
	}

	// 6. Success: increment counters, cache
	state.FirecrawlCallCount++
	state.FirecrawlPagesUsed += res.PagesUsed
	if state.FirecrawlRemaining != nil {
		*state.FirecrawlRemaining -= res.PagesUsed
	}
	state.FirecrawlCache[url] = res

	return marshalFirecrawlResult(res), nil
}

func firecrawlErrorJSON(code, message string, retryable bool) string {
	b, _ := json.Marshal(map[string]any{
		"error_code": code,
		"error":      message,
		"retryable":  retryable,
	})
	return string(b)
}

func marshalFirecrawlResult(r *scrape.FirecrawlResult) string {
	b, _ := json.Marshal(r)
	return string(b)
}

// --- helpers ---

func strArg(m map[string]any, k string) string  { return strFrom(m, k) }
func strFrom(m map[string]any, k string) string { s, _ := m[k].(string); return s }
func intArg(m map[string]any, k string) int {
	switch v := m[k].(type) {
	case int:
		return v
	case float64:
		return int(v)
	case int64:
		return int(v)
	}
	return 0
}

func errResult(code, msg string, retryable bool, hint string) string {
	b, _ := json.Marshal(map[string]any{
		"error":         msg,
		"error_code":    code,
		"retryable":     retryable,
		"fallback_hint": hint,
	})
	return string(b)
}

func fetchErrToJSON(err error) string {
	if fe, ok := err.(*scrape.FetchError); ok {
		return errResult(fe.Code, fe.Message, fe.Retryable, fe.FallbackHint)
	}
	return errResult("unknown_error", err.Error(), false, "")
}

func managerErrToJSON(err error) string {
	if me, ok := err.(*browser.ManagerError); ok {
		return errResult(me.Code, me.Message, false, "")
	}
	return errResult("unknown_error", err.Error(), false, "")
}
