// Optovia MCP server (Node) that proxies existing GraphQL endpoints with token pass-through.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as z from "zod";

const server = new McpServer({
  name: "optovia-mcp",
  version: "0.1.0",
});

/**
 * Execute a GraphQL request with optional Bearer token.
 */
async function gqlRequest(url, query, variables = {}, token) {
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
    body: JSON.stringify({ query, variables }),
  });

  const json = await res.json();
  if (!res.ok || json.errors) {
    const errMsg = json?.errors?.map((e) => e.message).join("; ") ?? res.statusText;
    throw new Error(`GraphQL error: ${errMsg}`);
  }
  return json.data;
}

/**
 * Decide which token to use.
 */
function resolveToken({ userToken, serviceTokenEnv, required }) {
  const token = userToken || process.env[serviceTokenEnv || ""];
  if (required && !token) {
    throw new Error("Authorization token is required for this operation.");
  }
  return token;
}

// Tool: search_products (Exchange service)
server.registerTool(
  "search_products",
  {
    title: "Search Products",
    description: "Search available products by name. Returns product UUIDs and names.",
    inputSchema: {
      query: z.string().optional().describe("Product name to search for (e.g. 'cocoa', 'coffee')"),
    },
  },
  async ({ query }) => {
    const exchangeUrl = process.env.EXCHANGE_GRAPHQL_URL;
    if (!exchangeUrl) {
      throw new Error("EXCHANGE_GRAPHQL_URL is not set.");
    }

    const gqlQuery = `
      query {
        getProducts {
          uuid
          name
          categoryName
        }
      }
    `;

    const data = await gqlRequest(exchangeUrl, gqlQuery, {}, null);
    let products = data.getProducts ?? [];

    if (query) {
      const q = query.toLowerCase();
      products = products.filter(p =>
        p.name?.toLowerCase().includes(q) ||
        p.categoryName?.toLowerCase().includes(q)
      );
    }

    return { content: [{ type: "text", text: JSON.stringify(products, null, 2) }] };
  }
);

// Tool: search_offers (Exchange service)
server.registerTool(
  "search_offers",
  {
    title: "Search Offers",
    description: "Search active offers on the exchange. Can filter by product, category, or location.",
    inputSchema: {
      productUuid: z.string().optional().describe("Filter by product UUID"),
      categoryName: z.string().optional().describe("Filter by category name (e.g. 'cocoa', 'coffee')"),
      locationUuid: z.string().optional().describe("Filter by location UUID"),
      limit: z.number().optional().default(10).describe("Limit results"),
    },
  },
  async ({ productUuid, categoryName, locationUuid, limit = 10 }) => {
    const exchangeUrl = process.env.EXCHANGE_GRAPHQL_URL;
    if (!exchangeUrl) {
      throw new Error("EXCHANGE_GRAPHQL_URL is not set.");
    }

    const gqlQuery = `
      query SearchOffers($productUuid: String, $categoryName: String, $locationUuid: String, $limit: Int) {
        getOffers(productUuid: $productUuid, categoryName: $categoryName, locationUuid: $locationUuid, limit: $limit) {
          uuid
          productUuid
          productName
          categoryName
          locationUuid
          locationName
          price
          currency
          quantity
          unit
          status
        }
      }
    `;

    const data = await gqlRequest(
      exchangeUrl,
      gqlQuery,
      { productUuid, categoryName, locationUuid, limit },
      null
    );

    return { content: [{ type: "text", text: JSON.stringify(data.getOffers ?? [], null, 2) }] };
  }
);

// Tool: match_offers_with_route
server.registerTool(
  "match_offers_with_route",
  {
    title: "Match Offers With Route",
    description: "Find active offers for a product and return route options to destination node.",
    inputSchema: {
      productUuid: z.string().describe("Product UUID"),
      destinationUuid: z.string().describe("Destination node UUID"),
      limitSources: z.number().optional().default(3).describe("Limit sources"),
      limitRoutes: z.number().optional().default(3).describe("Limit routes per source"),
      userToken: z.string().optional().describe("Optional Bearer token"),
    },
  },
  async ({ productUuid, destinationUuid, limitSources = 3, limitRoutes = 3, userToken }) => {
    const geoUrl = process.env.GEO_GRAPHQL_URL;
    if (!geoUrl) {
      throw new Error("GEO_GRAPHQL_URL is not set.");
    }

    const token = resolveToken({
      userToken,
      serviceTokenEnv: "GEO_M2M_TOKEN",
      required: false,
    });

    const query = `
      query MatchOffersRoute($productUuid: String!, $toUuid: String!, $limitSources: Int, $limitRoutes: Int) {
        find_product_routes(product_uuid: $productUuid, to_uuid: $toUuid, limit_sources: $limitSources, limit_routes: $limitRoutes) {
          source_uuid
          source_name
          source_lat
          source_lon
          distance_km
          routes {
            total_distance_km
            total_time_seconds
            stages {
              from_uuid
              from_name
              from_lat
              from_lon
              to_uuid
              to_name
              to_lat
              to_lon
              distance_km
              travel_time_seconds
              transport_type
            }
          }
        }
      }
    `;

    const data = await gqlRequest(
      geoUrl,
      query,
      {
        productUuid,
        toUuid: destinationUuid,
        limitSources,
        limitRoutes,
      },
      token
    );

    return { content: [{ type: "text", text: JSON.stringify(data.find_product_routes ?? [], null, 2) }] };
  }
);

// Tool: search_nodes (GEO nodes list)
server.registerTool(
  "search_nodes",
  {
    title: "Search Nodes",
    description: "Search logistics nodes by name or transport type. Use query to search by location name (e.g. 'Nairobi', 'Moscow').",
    inputSchema: {
      query: z.string().optional().describe("Search by node/location name (e.g. 'Nairobi', 'Moscow')"),
      transportType: z.string().optional().describe("Filter by transport type: auto | rail"),
      limit: z.number().optional().default(20).describe("Limit results"),
      offset: z.number().optional().default(0).describe("Offset for pagination"),
      userToken: z.string().optional().describe("Optional Bearer token"),
    },
  },
  async ({ query: searchQuery, transportType, limit = 20, offset = 0, userToken }) => {
    const geoUrl = process.env.GEO_GRAPHQL_URL;
    if (!geoUrl) {
      throw new Error("GEO_GRAPHQL_URL is not set.");
    }

    const token = resolveToken({
      userToken,
      serviceTokenEnv: "GEO_M2M_TOKEN",
      required: false,
    });

    const gqlQuery = `
      query Nodes($limit: Int, $offset: Int, $transport: String, $search: String) {
        nodes(limit: $limit, offset: $offset, transportType: $transport, search: $search) {
          uuid
          name
          latitude
          longitude
          country
          countryCode
          transportTypes
        }
      }
    `;

    const data = await gqlRequest(
      geoUrl,
      gqlQuery,
      { limit, offset, transport: transportType, search: searchQuery },
      token
    );

    return { content: [{ type: "text", text: JSON.stringify(data.nodes ?? [], null, 2) }] };
  }
);

// Tool: nearest_nodes (GEO - find nodes by coordinates)
server.registerTool(
  "nearest_nodes",
  {
    title: "Nearest Nodes",
    description: "Find nearest logistics nodes to given coordinates. Use this when searching by city name returns no results - provide approximate coordinates instead.",
    inputSchema: {
      lat: z.number().describe("Latitude of the location"),
      lon: z.number().describe("Longitude of the location"),
      limit: z.number().optional().default(5).describe("Max number of results"),
      userToken: z.string().optional().describe("Optional Bearer token"),
    },
  },
  async ({ lat, lon, limit = 5, userToken }) => {
    const geoUrl = process.env.GEO_GRAPHQL_URL;
    if (!geoUrl) {
      throw new Error("GEO_GRAPHQL_URL is not set.");
    }

    const token = resolveToken({
      userToken,
      serviceTokenEnv: "GEO_M2M_TOKEN",
      required: false,
    });

    const gqlQuery = `
      query NearestNodes($lat: Float!, $lon: Float!, $limit: Int) {
        nearestNodes(lat: $lat, lon: $lon, limit: $limit) {
          uuid
          name
          latitude
          longitude
          country
          countryCode
          transportTypes
        }
      }
    `;

    const data = await gqlRequest(
      geoUrl,
      gqlQuery,
      { lat, lon, limit },
      token
    );

    return { content: [{ type: "text", text: JSON.stringify(data.nearestNodes ?? [], null, 2) }] };
  }
);

// Tool: order_timeline (orders service)
server.registerTool(
  "order_timeline",
  {
    title: "Order Timeline",
    description: "Get order with stages/trips for a team. Requires user/team access token.",
    inputSchema: {
      orderUuid: z.string().describe("Order UUID"),
      userToken: z.string().describe("Required Bearer token with team scope (teams:member)"),
    },
  },
  async ({ orderUuid, userToken }) => {
    const ordersUrl = process.env.ORDERS_GRAPHQL_URL;
    if (!ordersUrl) {
      throw new Error("ORDERS_GRAPHQL_URL is not set.");
    }

    const token = resolveToken({
      userToken,
      required: true,
    });

    const query = `
      query Order($orderUuid: String!) {
        getOrder(orderUuid: $orderUuid) {
          uuid
          name
          teamUuid
          userId
          status
          totalAmount
          currency
          sourceLocationName
          destinationLocationName
          createdAt
          updatedAt
          notes
          stages {
            uuid
            name
            sequence
            stageType
            transportType
            sourceLocationName
            destinationLocationName
            locationName
            selectedCompany { uuid name }
            trips {
              uuid
              name
              sequence
              plannedLoadingDate
              actualLoadingDate
              realLoadingDate
              plannedUnloadingDate
              actualUnloadingDate
              plannedWeight
              weightAtLoading
              weightAtUnloading
              company { uuid name }
            }
          }
        }
      }
    `;

    const data = await gqlRequest(ordersUrl, query, { orderUuid }, token);
    return { content: [{ type: "text", text: JSON.stringify(data.getOrder, null, 2) }] };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((err) => {
  console.error("MCP server failed:", err);
  process.exit(1);
});
