export * as GrepTool from "./grep"

import { ToolFailure, toolText } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { FileSystem } from "../filesystem"
import { LocationSearch } from "../location-search"
import { Ripgrep } from "../ripgrep"
import { PermissionV2 } from "../permission"
import { Tool } from "./tool"
import { Tools } from "./tools"

export const name = "grep"

export const Input = Schema.Struct({
  pattern: LocationSearch.GrepInput.fields.pattern.annotate({
    description: "Regex pattern to search for in file contents",
  }),
  path: LocationSearch.GrepInput.fields.path.annotate({
    description: "Relative file or directory to search. Defaults to the active Location.",
  }),
  reference: LocationSearch.GrepInput.fields.reference.annotate({
    description: "Named project reference to search instead of the active Location",
  }),
  include: LocationSearch.GrepInput.fields.include.annotate({
    description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")',
  }),
  limit: LocationSearch.GrepInput.fields.limit.annotate({
    description: `Maximum matches to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})`,
  }),
})

type Output = typeof LocationSearch.GrepResult.Encoded

/** Format raw Location search matches into the familiar concise model output. */
export const toModelOutput = (output: Output) => {
  const lines = output.items.length === 0 ? ["No files found"] : [`Found ${output.items.length} matches`]
  let current = ""
  for (const match of output.items) {
    if (current !== match.resource) {
      if (current) lines.push("")
      current = match.resource
      lines.push(`${match.resource}:`)
    }
    lines.push(`  Line ${match.line}: ${match.lines}${match.linePreviewTruncated ? "..." : ""}`)
  }
  if (output.truncated) {
    lines.push(
      "",
      `(Results are truncated: showing first ${output.items.length} matches. Consider using a more specific path or pattern.)`,
    )
  }
  if (output.partial) lines.push("", "(Some paths were inaccessible and skipped)")
  return lines.join("\n")
}

/**
 * Location-scoped grep leaf. FileSystem supplies canonical permission metadata;
 * LocationSearch resolves the current root and owns containment and ripgrep execution.
 *
 * TODO: Revisit root-specific search permission resources if named-reference policy needs independent allow/deny rules.
 */
export const layer = Layer.effectDiscard(
  Effect.gen(function* () {
    const tools = yield* Tools.Service
    const filesystem = yield* FileSystem.Service
    const search = yield* LocationSearch.Service
    const permission = yield* PermissionV2.Service

    yield* tools
      .register({
        [name]: Tool.make({
          description:
            "Search file contents by regular expression within the active Location, a named project reference, or an absolute managed tool-output file. Use a path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise file resources, line numbers, and bounded line previews.",
          input: Input,
          output: LocationSearch.GrepResult,
          toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
          execute: (input, context) =>
            Effect.gen(function* () {
              const root = yield* filesystem.resolveRoot(input)
              yield* permission.assert({
                action: name,
                resources: [input.pattern],
                save: ["*"],
                metadata: {
                  root: root.resource,
                  reference: input.reference,
                  path: input.path,
                  include: input.include,
                  limit: input.limit,
                },
                sessionID: context.sessionID,
                agent: context.agent,
                source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
              })
              return yield* search.grep(input)
            }).pipe(
              Effect.mapError((error) => {
                const message =
                  error instanceof Ripgrep.InvalidPatternError
                    ? `Invalid grep pattern ${JSON.stringify(input.pattern)}: ${error.message}`
                    : `Unable to grep for ${input.pattern}`
                return new ToolFailure({ message })
              }),
            ),
        }),
      })
      .pipe(Effect.orDie)
  }),
)
