import { fileURLToPath } from "url"
import { Effect, Context, Layer, Exit, Cause, Scope, Types } from "effect"
import { SessionID, PartID } from "./schema"
import { MessageV2 } from "../message/message-v2"
import * as Session from "./session"
import { Provider } from "@/provider/provider"
import { Bus } from "../../bus"
import { ToolRegistry } from "@/tool/registry"
import { MCP } from "../../mcp"
import { LSP } from "@/lsp/lsp"
import { Plugin } from "../../plugin"
import { AppFileSystem } from "@saeeol/core/filesystem"
import { decodeDataUrl } from "@/util/data-url"
import { Permission } from "@/permission"
import { NamedError } from "@saeeol/core/util/error"
import * as Log from "@saeeol/core/util/log"
import type { PromptInput } from "../prompt/prompt-schemas"

const log = Log.create({ service: "session.user-part" })

export interface ResolveUserPartDeps {
  sessionID: SessionID
  messageID: ReturnType<typeof MessageV2.Info.make>["id"]
  agent: string
  agentPermission: any
  model: { providerID: any; modelID: any }
}

export interface Interface {
  readonly resolve: (
    part: PromptInput["parts"][number],
    deps: ResolveUserPartDeps,
  ) => Effect.Effect<any[], never, Scope.Scope>
}
export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionUserPart") {}

export const layer = Layer.effect(
  Service,
  Effect.gen(function* () {
    const fsys = yield* AppFileSystem.Service
    const mcp = yield* MCP.Service
    const lsp = yield* LSP.Service
    const registry = yield* ToolRegistry.Service
    const plugin = yield* Plugin.Service
    const provider = yield* Provider.Service
    const bus = yield* Bus.Service

    const resolve = Effect.fn("SessionUserPart.resolve")(function* (
      part: PromptInput["parts"][number],
      deps: ResolveUserPartDeps,
    ) {
      const { sessionID, messageID, agent, agentPermission, model } = deps

      if (part.type === "file") {
        if (part.source?.type === "resource") {
          const { clientName, uri } = part.source
          log.info("mcp resource", { clientName, uri, mime: part.mime })
          const pieces: any[] = [
            {
              messageID, sessionID,
              type: "text", synthetic: true,
              text: `Reading MCP resource: ${part.filename} (${uri})`,
            },
          ]
          const exit = yield* mcp.readResource(clientName, uri).pipe(Effect.exit)
          if (Exit.isSuccess(exit)) {
            const content = exit.value
            if (!content) throw new Error(`Resource not found: ${clientName}/${uri}`)
            const items = Array.isArray(content.contents) ? content.contents : [content.contents]
            for (const c of items) {
              if ("text" in c && c.text) {
                pieces.push({
                  messageID, sessionID,
                  type: "text", synthetic: true, text: c.text,
                })
              } else if ("blob" in c && c.blob) {
                const mime = "mimeType" in c ? c.mimeType : part.mime
                pieces.push({
                  messageID, sessionID,
                  type: "text", synthetic: true,
                  text: `[Binary content: ${mime}]`,
                })
              }
            }
            pieces.push({ ...part, messageID, sessionID })
          } else {
            const error = Cause.squash(exit.cause)
            log.error("failed to read MCP resource", { error, clientName, uri })
            const message = error instanceof Error ? error.message : String(error)
            pieces.push({
              messageID, sessionID,
              type: "text", synthetic: true,
              text: `Failed to read MCP resource ${part.filename}: ${message}`,
            })
          }
          return pieces
        }

        const url = new URL(part.url)
        switch (url.protocol) {
          case "data:":
            if (part.mime === "text/plain") {
              return [
                {
                  messageID, sessionID,
                  type: "text", synthetic: true,
                  text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
                },
                {
                  messageID, sessionID,
                  type: "text", synthetic: true,
                  text: decodeDataUrl(part.url),
                },
                { ...part, messageID, sessionID },
              ]
            }
            break
          case "file:": {
            log.info("file", { mime: part.mime })
            const filepath = fileURLToPath(part.url)
            const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime

            const { read } = yield* registry.named()
            const execRead = (args: Parameters<typeof read.execute>[0], extra?: any) => {
              const controller = new AbortController()
              return read
                .execute(args, {
                  sessionID, abort: controller.signal,
                  agent, messageID,
                  extra: { bypassCwdCheck: true, ...extra },
                  messages: [], metadata: () => Effect.void, ask: () => Effect.void,
                })
                .pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
            }

            if (mime === "text/plain") {
              let offset: number | undefined
              let limit: number | undefined
              const range = { start: url.searchParams.get("start"), end: url.searchParams.get("end") }
              if (range.start != null) {
                const filePathURI = part.url.split("?")[0]
                let start = parseInt(range.start)
                let end = range.end ? parseInt(range.end) : undefined
                if (start === end) {
                  const symbols = yield* lsp.documentSymbol(filePathURI).pipe(Effect.catch(() => Effect.succeed([])))
                  for (const symbol of symbols) {
                    let r: LSP.Range | undefined
                    if ("range" in symbol) r = symbol.range
                    else if ("location" in symbol) r = symbol.location.range
                    if (r?.start?.line && r?.start?.line === start) {
                      start = r.start.line
                      end = r?.end?.line ?? start
                      break
                    }
                  }
                }
                offset = Math.max(start, 1)
                if (end) limit = end - (offset - 1)
              }
              const args = { filePath: filepath, offset, limit }
          const pieces: any[] = [
                {
                  messageID, sessionID,
                  type: "text", synthetic: true,
                  text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
                },
              ]
              const exit = yield* provider.getModel(model.providerID, model.modelID).pipe(
                Effect.flatMap((mdl) => execRead(args, { model: mdl })),
                Effect.exit,
              )
              if (Exit.isSuccess(exit)) {
                const result = exit.value
                pieces.push({
                  messageID, sessionID,
                  type: "text", synthetic: true, text: result.output,
                })
                if (result.attachments?.length) {
                  pieces.push(
                    ...result.attachments.map((a: any) => ({
                      ...a, synthetic: true,
                      filename: a.filename ?? part.filename,
                      messageID, sessionID,
                    })),
                  )
                } else {
                  pieces.push({ ...part, mime, messageID, sessionID })
                }
              } else {
                const error = Cause.squash(exit.cause)
                log.error("failed to read file", { error })
                const message = error instanceof Error ? error.message : String(error)
                yield* bus.publish(Session.Event.Error, {
                  sessionID,
                  error: new NamedError.Unknown({ message }).toObject(),
                })
                pieces.push({
                  messageID, sessionID,
                  type: "text", synthetic: true,
                  text: `Read tool failed to read ${filepath} with the following error: ${message}`,
                })
              }
              return pieces
            }

            if (mime === "application/x-directory") {
              const args = { filePath: filepath }
              const exit = yield* execRead(args, { includeDirectoryFiles: true }).pipe(Effect.exit)
              if (Exit.isFailure(exit)) {
                const error = Cause.squash(exit.cause)
                log.error("failed to read directory", { error })
                const message = error instanceof Error ? error.message : String(error)
                yield* bus.publish(Session.Event.Error, {
                  sessionID,
                  error: new NamedError.Unknown({ message }).toObject(),
                })
                return [
                  {
                    messageID, sessionID,
                    type: "text", synthetic: true,
                    text: `Read tool failed to read ${filepath} with the following error: ${message}`,
                  },
                ]
              }
              return [
                {
                  messageID, sessionID,
                  type: "text", synthetic: true,
                  text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
                },
                {
                  messageID, sessionID,
                  type: "text", synthetic: true,
                  text: exit.value.output,
                },
                { ...part, mime, messageID, sessionID },
              ]
            }

            return [
              {
                messageID, sessionID,
                type: "text", synthetic: true,
                text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`,
              },
              {
                id: part.id, messageID, sessionID,
                type: "file",
                url:
                  `data:${mime};base64,` +
                  Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"),
                mime,
                filename: part.filename!,
                source: part.source,
              },
            ]
          }
        }
      }

      if (part.type === "agent") {
        const perm = Permission.evaluate("task", part.name, agentPermission)
        const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
        return [
          { ...part, messageID, sessionID } as any,
          {
            messageID, sessionID,
            type: "text", synthetic: true,
            text:
              " Use the above message and context to generate a prompt and call the task tool with subagent: " +
              part.name + hint,
          },
        ]
      }

      return [{ ...part, messageID, sessionID } as any]
    })

    return { resolve }
  }),
)

export const defaultLayer = Layer.suspend(() =>
  layer.pipe(
    Layer.provide(AppFileSystem.defaultLayer),
    Layer.provide(MCP.defaultLayer),
    Layer.provide(LSP.defaultLayer),
    Layer.provide(ToolRegistry.defaultLayer),
    Layer.provide(Plugin.defaultLayer),
    Layer.provide(Provider.defaultLayer),
    Layer.provide(Bus.layer),
  ),
)
