import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { writeAudit, type Db } from '@znet/catalog-db'
import { PublicError, type Logger } from '@znet/shared'
import { MagentoApiError, type MagentoApi } from '@znet/magento'
import { callerOf, type Caller } from './auth.js'
import type { CatalogApiClient } from './catalog-client.js'
import type { McpEnv, Scope } from './config.js'
import { redactArgs } from './pii.js'

export interface ToolDeps {
  catalog: CatalogApiClient
  magento: MagentoApi
  db: Db
  env: McpEnv
  log: Logger
}

export interface ToolContext extends ToolDeps {
  caller: Caller
}

export interface ToolDef<Shape extends z.ZodRawShape = z.ZodRawShape> {
  name: string
  title: string
  description: string
  scope: Scope
  input: Shape
  run: (args: z.infer<z.ZodObject<Shape>>, ctx: ToolContext) => Promise<unknown>
}

export const defineTool = <Shape extends z.ZodRawShape>(def: ToolDef<Shape>) => def as unknown as ToolDef

/** Keep tool output small enough for a chat model: cap long strings. */
function compact(value: unknown, maxString = 2000): unknown {
  if (typeof value === 'string') return value.length > maxString ? `${value.slice(0, maxString)}… [truncated]` : value
  if (Array.isArray(value)) return value.map(v => compact(v, maxString))
  if (value && typeof value === 'object' && !(value instanceof Date)) {
    return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, compact(v, maxString)]))
  }
  return value
}

const asText = (data: unknown): CallToolResult => ({ content: [{ type: 'text', text: JSON.stringify(compact(data), null, 1) }] })
const asError = (message: string): CallToolResult => ({ isError: true, content: [{ type: 'text', text: message }] })

function publicMessage(err: unknown): string | null {
  if (err instanceof PublicError) return err.message
  if (err instanceof MagentoApiError) {
    if (err.status === 404) return 'Not found in Magento.'
    if (err.status === 401 || err.status === 403) return 'Magento refused the request (integration permissions).'
    return 'Magento is not responding normally right now; try again shortly.'
  }
  if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')) return 'The request timed out; try again shortly.'
  return null
}

/**
 * Register the tools the caller's scopes allow. Tools outside their scopes are not listed at all.
 * Every call is audited (with PII-redacted arguments) and errors are mapped to safe messages.
 */
export function registerTools(server: McpServer, tools: ToolDef[], deps: ToolDeps, caller: Caller) {
  for (const tool of tools) {
    if (!caller.scopes.includes(tool.scope)) continue
    server.registerTool(
      tool.name,
      {
        title: tool.title,
        description: tool.description,
        inputSchema: tool.input,
        annotations: { title: tool.title, readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
      },
      async (args: Record<string, unknown>, extra): Promise<CallToolResult> => {
        const who = extra.authInfo ? callerOf(extra.authInfo) : caller
        const started = Date.now()
        let outcome: 'ok' | 'error' = 'ok'
        let error: string | null = null
        try {
          return asText(await tool.run(args as never, { ...deps, caller: who }))
        } catch (err) {
          outcome = 'error'
          error = err instanceof Error ? err.message : String(err)
          const message = publicMessage(err)
          if (!message) deps.log.error({ err, tool: tool.name }, 'tool failed')
          return asError(message ?? 'Something went wrong running this tool.')
        } finally {
          writeAudit(deps.db, {
            subject: who.subject, clientId: who.clientId, tool: tool.name, args: redactArgs(args), outcome, error, durationMs: Date.now() - started,
          }).catch(err => deps.log.error({ err }, 'audit write failed'))
        }
      },
    )
  }
}
