import { FastifyInstance } from "fastify"
import { and, desc, eq } from "drizzle-orm"
import { createHash, randomBytes } from "node:crypto"
import { m2mApiKeys } from "../../db/schema"
import { assertToolPermission, createMcpContext } from "../mcp/authz"
import { mcpToolMap, mcpTools } from "../mcp/registry"
import { asToolError, asToolResult } from "../mcp/result"
import { JsonRpcRequest } from "../mcp/types"

const SUPPORTED_PROTOCOL_VERSIONS = [
    "2025-11-25",
    "2025-06-18",
    "2025-03-26",
    "2024-11-05",
]

function jsonRpcResult(id: JsonRpcRequest["id"], result: unknown) {
    return {
        jsonrpc: "2.0",
        id,
        result,
    }
}

function jsonRpcError(id: JsonRpcRequest["id"], code: number, message: string) {
    return {
        jsonrpc: "2.0",
        id: id ?? null,
        error: {
            code,
            message,
        },
    }
}

function selectProtocolVersion(clientVersion?: string) {
    if (clientVersion && SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion)) {
        return clientVersion
    }

    return SUPPORTED_PROTOCOL_VERSIONS[0]
}

const hashApiKey = (apiKey: string) =>
    createHash("sha256").update(apiKey, "utf8").digest("hex")

const createMcpToken = () => `fedeo_mcp_${randomBytes(32).toString("base64url")}`

const requireMcpTokenManagementPermission = (req: any) => {
    if (req.mcpTokenAuth) {
        throw Object.assign(new Error("MCP Tokens können nur mit einer Benutzersitzung verwaltet werden"), { statusCode: 403 })
    }

    if (req.user?.is_admin) return
    if (typeof req.hasPermission === "function" && req.hasPermission("mcp.tokens.write")) return

    throw Object.assign(new Error("Fehlende Berechtigung: mcp.tokens.write"), { statusCode: 403 })
}

export default async function mcpRoutes(server: FastifyInstance) {
    server.get("/mcp/tokens", async (req, reply) => {
        try {
            requireMcpTokenManagementPermission(req)

            if (!req.user?.tenant_id) {
                return reply.code(400).send({ error: "No tenant selected" })
            }

            const rows = await server.db
                .select({
                    id: m2mApiKeys.id,
                    createdAt: m2mApiKeys.createdAt,
                    updatedAt: m2mApiKeys.updatedAt,
                    name: m2mApiKeys.name,
                    keyPrefix: m2mApiKeys.keyPrefix,
                    active: m2mApiKeys.active,
                    lastUsedAt: m2mApiKeys.lastUsedAt,
                    expiresAt: m2mApiKeys.expiresAt,
                    userId: m2mApiKeys.userId,
                    createdBy: m2mApiKeys.createdBy,
                })
                .from(m2mApiKeys)
                .where(and(
                    eq(m2mApiKeys.tenantId, req.user.tenant_id)
                ))
                .orderBy(desc(m2mApiKeys.createdAt))

            return { rows: rows.filter((row) => row.keyPrefix.startsWith("fedeo_mcp_")) }
        } catch (error) {
            const statusCode = (error as any)?.statusCode || 500
            return reply.code(statusCode).send({ error: error instanceof Error ? error.message : "Internal Server Error" })
        }
    })

    server.post("/mcp/tokens", async (req, reply) => {
        try {
            requireMcpTokenManagementPermission(req)

            if (!req.user?.tenant_id || !req.user?.user_id) {
                return reply.code(400).send({ error: "No tenant selected" })
            }

            const body = req.body as {
                name?: string
                expiresAt?: string | null
            }

            const token = createMcpToken()
            const expiresAt = body?.expiresAt ? new Date(body.expiresAt) : null

            if (expiresAt && Number.isNaN(expiresAt.getTime())) {
                return reply.code(400).send({ error: "expiresAt must be a valid date" })
            }

            const [created] = await server.db
                .insert(m2mApiKeys)
                .values({
                    tenantId: req.user.tenant_id,
                    userId: req.user.user_id,
                    createdBy: req.user.user_id,
                    name: body?.name?.trim() || "FEDEO MCP Token",
                    keyPrefix: token.slice(0, 20),
                    keyHash: hashApiKey(token),
                    active: true,
                    expiresAt,
                })
                .returning({
                    id: m2mApiKeys.id,
                    createdAt: m2mApiKeys.createdAt,
                    name: m2mApiKeys.name,
                    keyPrefix: m2mApiKeys.keyPrefix,
                    active: m2mApiKeys.active,
                    expiresAt: m2mApiKeys.expiresAt,
                })

            return {
                token,
                tokenType: "Bearer",
                note: "Token wird nur einmal angezeigt. Bitte direkt in Codex oder einem Secret Store hinterlegen.",
                record: created,
            }
        } catch (error) {
            const statusCode = (error as any)?.statusCode || 500
            return reply.code(statusCode).send({ error: error instanceof Error ? error.message : "Internal Server Error" })
        }
    })

    server.delete("/mcp/tokens/:id", async (req, reply) => {
        try {
            requireMcpTokenManagementPermission(req)

            if (!req.user?.tenant_id || !req.user?.user_id) {
                return reply.code(400).send({ error: "No tenant selected" })
            }

            const { id } = req.params as { id: string }

            const [existing] = await server.db
                .select({
                    id: m2mApiKeys.id,
                    keyPrefix: m2mApiKeys.keyPrefix,
                })
                .from(m2mApiKeys)
                .where(and(
                    eq(m2mApiKeys.id, id),
                    eq(m2mApiKeys.tenantId, req.user.tenant_id)
                ))
                .limit(1)

            if (!existing || !existing.keyPrefix.startsWith("fedeo_mcp_")) {
                return reply.code(404).send({ error: "MCP token not found" })
            }

            const [updated] = await server.db
                .update(m2mApiKeys)
                .set({
                    active: false,
                    updatedAt: new Date(),
                })
                .where(and(
                    eq(m2mApiKeys.id, id),
                    eq(m2mApiKeys.tenantId, req.user.tenant_id)
                ))
                .returning({
                    id: m2mApiKeys.id,
                    name: m2mApiKeys.name,
                    keyPrefix: m2mApiKeys.keyPrefix,
                    active: m2mApiKeys.active,
                })

            return { token: updated }
        } catch (error) {
            const statusCode = (error as any)?.statusCode || 500
            return reply.code(statusCode).send({ error: error instanceof Error ? error.message : "Internal Server Error" })
        }
    })

    server.post("/mcp", async (req, reply) => {
        const body = req.body as JsonRpcRequest | JsonRpcRequest[]
        const requests = Array.isArray(body) ? body : [body]
        const responses = []

        for (const request of requests) {
            const id = request?.id

            if (!request || request.jsonrpc !== "2.0" || !request.method) {
                responses.push(jsonRpcError(id, -32600, "Invalid JSON-RPC request"))
                continue
            }

            if (request.method === "notifications/initialized") {
                continue
            }

            if (request.method === "initialize") {
                const clientVersion = request.params?.protocolVersion

                responses.push(jsonRpcResult(id, {
                    protocolVersion: selectProtocolVersion(clientVersion),
                    capabilities: {
                        tools: {
                            listChanged: false,
                        },
                    },
                    serverInfo: {
                        name: "fedeo-mcp",
                        version: "1.0.0",
                    },
                    instructions: "FEDEO MCP-Server für mandantenbezogene Buchhaltungs- und Organisationswerkzeuge. Alle Tools prüfen Rollenberechtigungen und arbeiten im aktiven Mandanten.",
                }))
                continue
            }

            if (request.method === "ping") {
                responses.push(jsonRpcResult(id, {}))
                continue
            }

            if (request.method === "tools/list") {
                responses.push(jsonRpcResult(id, {
                    tools: mcpTools.map((tool) => ({
                        name: tool.name,
                        title: tool.title,
                        description: tool.description,
                        inputSchema: tool.inputSchema,
                        annotations: {
                            readOnlyHint: !tool.requiredPermissions.some((permission) => permission.endsWith(".write")),
                        },
                    })),
                }))
                continue
            }

            if (request.method === "tools/call") {
                const toolName = request.params?.name
                const tool = typeof toolName === "string" ? mcpToolMap.get(toolName) : null

                if (!tool) {
                    responses.push(jsonRpcError(id, -32602, `Unknown tool: ${toolName}`))
                    continue
                }

                try {
                    const context = await createMcpContext(server, req)
                    assertToolPermission(context, tool)

                    const result = await tool.handler(context, request.params?.arguments || {})
                    responses.push(jsonRpcResult(id, asToolResult(result)))
                } catch (error) {
                    const statusCode = (error as any)?.statusCode

                    if (statusCode === 401 || statusCode === 403) {
                        responses.push(jsonRpcError(id, statusCode === 401 ? -32001 : -32003, error instanceof Error ? error.message : "Forbidden"))
                    } else {
                        responses.push(jsonRpcResult(id, asToolError(error)))
                    }
                }
                continue
            }

            responses.push(jsonRpcError(id, -32601, `Method not found: ${request.method}`))
        }

        if (responses.length === 0) {
            return reply.code(204).send()
        }

        return Array.isArray(body) ? responses : responses[0]
    })

    server.get("/mcp", async (_req, reply) => {
        return reply.send({
            name: "fedeo-mcp",
            transport: "http-json-rpc",
            endpoint: "/api/mcp",
            tools: mcpTools.map((tool) => tool.name),
        })
    })
}
