import { BestAgentError, normalizeWhitespace } from "@best-agent/foundation";
import type { ToolInputValue } from "@best-agent/protocol";
import { z } from "zod";
import type { ToolDefinition, ToolInputAdapter } from "../contracts.js";
import { typedToolInputSchema } from "../schema.js";
import { isPlainObject, readStringField } from "./shared.js";

const FETCH_URL_TIMEOUT_MS = 30_000;
const FETCH_URL_RICH_TEXT_LIMIT = 8_000;

type UrlInput = { format: "rich" | "text"; url: string };
type WebSearchInput = { domains?: string[]; limit: number; query: string; recency_days?: number };

export function createFetchUrlTool(): ToolDefinition<UrlInput> {
  return {
    description:
      "Fetch a URL over HTTP or HTTPS. Rich mode returns citation-ready metadata alongside the response text.",
    effect: "read",
    executionClass: "read-only",
    idempotent: true,
    inputSchema: typedToolInputSchema<UrlInput>(
      z.strictObject({
        format: z.enum(["text", "rich"]).default("text"),
        url: z.string(),
      }),
    ),
    name: "fetch-url",
    source: "built-in",
    capabilities(input) {
      const url = new URL(input.url.trim());
      return [
        {
          group: "network",
          host: url.hostname,
        },
      ];
    },
    async execute(input, context) {
      const url = new URL(input.url.trim());
      const controller = new AbortController();
      const timeout = setTimeout(() => {
        controller.abort(
          new BestAgentError(
            "FETCH_FAILED",
            `Request timed out after ${FETCH_URL_TIMEOUT_MS}ms. URL: ${url.toString()}`,
          ),
        );
      }, FETCH_URL_TIMEOUT_MS);
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) {
          const failedUrl = response.url || url.toString();
          throw new BestAgentError(
            "FETCH_FAILED",
            `Request failed with status ${response.status}. URL: ${failedUrl}`,
          );
        }
        const text = await response.text();
        if (input.format === "text") {
          return text;
        }
        const richText = truncateFetchUrlRichText(text);
        const richOutput = JSON.stringify(
          {
            content_type: response.headers.get("content-type"),
            final_url: response.url || url.toString(),
            retrieved_at: new Date().toISOString(),
            text: richText.text,
            text_length: text.length,
            title: deriveFetchedTitle(text, response.headers.get("content-type")),
            truncated: richText.truncated,
          },
          null,
          2,
        );
        context.setResultMetadata?.({
          outputManaged: true,
        });
        return richOutput;
      } finally {
        clearTimeout(timeout);
      }
    },
    formatInput(input) {
      return input.format === "text" ? input.url : `${input.url} (rich)`;
    },
    legacyAdapter: {
      adapt(input) {
        return normalizeUrlInput("fetch-url", input);
      },
    } satisfies ToolInputAdapter<UrlInput>,
  };
}

export function createWebSearchTool(): ToolDefinition<WebSearchInput> {
  return {
    description:
      "Search the web for external sources. If this runtime has no search capability, returns a soft-unavailable guidance message instead of failing.",
    effect: "search",
    executionClass: "read-only",
    idempotent: true,
    inputSchema: typedToolInputSchema<WebSearchInput>(
      z.strictObject({
        domains: z.array(z.string().min(1)).optional(),
        limit: z.int().min(1).max(10).default(5),
        query: z.string().min(1),
        recency_days: z.int().min(1).optional(),
      }),
    ),
    name: "web-search",
    source: "built-in",
    capabilities() {
      return [];
    },
    async execute(_input, context) {
      const priorCalls = await context.store.listItems(context.threadId);
      const currentTurnCalls = priorCalls.filter(
        (item) =>
          item.turnId === context.turnId &&
          item.kind === "tool-call" &&
          item.toolName === "web-search",
      ).length;
      if (currentTurnCalls > 2) {
        return "Web search budget exhausted for this turn. Use fetch-url with a known URL or ask the user for a source.";
      }
      if (
        context.webSearchBudget &&
        context.webSearchBudget.currentCount > context.webSearchBudget.limit
      ) {
        return "Web search budget exhausted for this workflow run. Use fetch-url with a known URL or ask the user for a source.";
      }
      if (!context.webSearch) {
        return "Web search is not available in this runtime. Use fetch-url with a known URL or ask the user for a source.";
      }
      return context.webSearch(_input);
    },
    formatInput(input) {
      return input.limit === 5 ? input.query : `${input.query} (limit ${input.limit})`;
    },
    legacyAdapter: {
      adapt(input) {
        return normalizeWebSearchInput(input);
      },
    } satisfies ToolInputAdapter<WebSearchInput>,
    summarize(input, output) {
      return output.includes("budget exhausted")
        ? output.includes("workflow run")
          ? `Tool web-search could not search for "${input.query}" because the workflow budget was exhausted.`
          : `Tool web-search could not search for "${input.query}" because the turn budget was exhausted.`
        : output.includes("not available in this runtime")
          ? `Tool web-search could not search for "${input.query}" because no search capability is configured.`
          : `Tool web-search searched the web for "${input.query}".`;
    },
  };
}

// ── Helpers ─────────────────────────────────────────────────────────────

function normalizeUrlInput(toolName: string, input: ToolInputValue): UrlInput {
  if (typeof input === "string") {
    return { format: "text", url: input };
  }
  if (!isPlainObject(input)) {
    throw new BestAgentError(
      "TOOL_INPUT_INVALID",
      `${toolName} requires an object input with a url field.`,
    );
  }

  const url = readStringField(input, ["url", "href"]);
  if (!url) {
    throw new BestAgentError("TOOL_INPUT_INVALID", `${toolName} requires a url field.`);
  }
  const format = input.format === "rich" ? "rich" : "text";
  return { format, url };
}

function normalizeWebSearchInput(input: ToolInputValue): WebSearchInput {
  if (typeof input === "string") {
    const query = input.trim();
    if (!query) {
      throw new BestAgentError("TOOL_INPUT_INVALID", "web-search requires a non-empty query.");
    }
    return {
      limit: 5,
      query,
    };
  }
  if (!isPlainObject(input)) {
    throw new BestAgentError(
      "TOOL_INPUT_INVALID",
      "web-search requires an object input with a query field.",
    );
  }
  const query = readStringField(input, ["query", "text", "search"]);
  if (!query) {
    throw new BestAgentError("TOOL_INPUT_INVALID", "web-search requires a query field.");
  }
  const rawLimit = typeof input.limit === "number" ? Math.trunc(input.limit) : 5;
  const domains = Array.isArray(input.domains)
    ? input.domains
        .filter((value): value is string => typeof value === "string")
        .map((value) => value.trim())
        .filter((value) => value.length > 0)
    : undefined;
  const recency_days =
    typeof input.recency_days === "number" &&
    Number.isInteger(input.recency_days) &&
    input.recency_days > 0
      ? input.recency_days
      : undefined;
  const normalized: WebSearchInput = {
    limit: Math.min(Math.max(rawLimit, 1), 10),
    query,
  };
  if (domains && domains.length > 0) {
    normalized.domains = domains;
  }
  if (recency_days !== undefined) {
    normalized.recency_days = recency_days;
  }
  return normalized;
}

function deriveFetchedTitle(text: string, content_type: string | null): string | null {
  if (!content_type?.toLowerCase().includes("text/html")) {
    return null;
  }
  const match = text.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
  if (!match?.[1]) {
    return null;
  }
  const normalized = normalizeWhitespace(match[1]);
  return normalized.length > 0 ? normalized : null;
}

function truncateFetchUrlRichText(text: string): { text: string; truncated: boolean } {
  if (text.length <= FETCH_URL_RICH_TEXT_LIMIT) {
    return { text, truncated: false };
  }
  return {
    text:
      `${text.slice(0, FETCH_URL_RICH_TEXT_LIMIT)}\n\n` +
      "[truncated] Rich fetch output clipped to stay within the model context budget.",
    truncated: true,
  };
}
