import * as fs from "fs";
import * as path from "path";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import {
  BatchSpanProcessor,
  type Span,
  type SpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import { metrics, type Counter } from "@opentelemetry/api";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { PrismaInstrumentation } from "@prisma/instrumentation";

// Mirrors the regex in middleware.ts — valid Server Action IDs are 40-char hex SHA-1 hashes.
const VALID_ACTION_ID = /^[0-9a-f]{40}$/;

// Populated after sdk.start() so the requestHook can increment it.
let blockedRequestsCounter: Counter | null = null;

// OAuth token endpoints — 4xx failures (e.g. invalid_grant) are expected,
// handled at the application level, and tracked in ServiceConnection.status.
// Skipping instrumentation avoids false-positive ERROR spans in SigNoz.
const OAUTH_TOKEN_URL_PATTERNS = [
  "oauth2.googleapis.com/token",
  "linkedin.com/oauth/v2/accessToken",
  "salesforce.com/services/oauth2/token",
  "atlassian.com/oauth/token",
  "hubapi.com/oauth/v1/token",
  "slack.com/api/oauth",
  "github.com/login/oauth/access_token",
  "twitter.com/oauth2/token",
  "api.twitter.com/2/oauth2",
  "graph.facebook.com/v21.0/oauth/access_token",
  "graph.threads.net/oauth/access_token",
  "graph.threads.net/refresh_access_token",
];

const SENSITIVE_QUERY_PARAM_PATTERN =
  /([?&])(access_token|client_secret|fb_exchange_token)=([^&]*)/g;

function redactSensitiveUrlValues(value: string): string {
  return value.replace(SENSITIVE_QUERY_PARAM_PATTERN, "$1$2=REDACTED");
}

class SensitiveUrlSpanProcessor implements SpanProcessor {
  onStart(span: Span): void {
    this.redactSpan(span);
  }

  onEnding(span: Span): void {
    this.redactSpan(span);
  }

  private redactSpan(span: Span): void {
    const redactedName = redactSensitiveUrlValues(span.name);
    if (redactedName !== span.name) {
      span.updateName(redactedName);
    }

    for (const key of ["http.url", "url.full"]) {
      const value = span.attributes[key];
      if (typeof value !== "string") continue;

      const redactedValue = redactSensitiveUrlValues(value);
      if (redactedValue !== value) {
        span.setAttribute(key, redactedValue);
      }
    }
  }

  onEnd(): void {}

  forceFlush(): Promise<void> {
    return Promise.resolve();
  }

  shutdown(): Promise<void> {
    return Promise.resolve();
  }
}

// Build route pattern matchers for normalising raw URL paths → Next.js route patterns.
// Primary source: .next/routes-manifest.json (present in both dev builds and production
// standalone output). Covers API routes, page routes, and correctly omits route groups.
// Fallback: walk src/app/ for dev servers that haven't been built yet.
function buildRouteMatchers(appDir: string): Array<[RegExp, string]> {
  const patterns: Array<[RegExp, string]> = [];

  // Primary: Next.js routes manifest — has correct regex + page for every route
  try {
    const manifestPath = path.join(process.cwd(), ".next", "routes-manifest.json");
    const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
      dynamicRoutes?: Array<{ page: string; regex: string }>;
      staticRoutes?: Array<{ page: string; regex: string }>;
    };

    for (const route of [
      ...(manifest.dynamicRoutes ?? []),
      ...(manifest.staticRoutes ?? []),
    ]) {
      try {
        patterns.push([new RegExp(route.regex), route.page]);
      } catch {
        // skip malformed regex entry
      }
    }

    if (patterns.length > 0) {
      // More-specific routes (more segments) should match before shorter prefixes
      patterns.sort((a, b) => b[1].split("/").length - a[1].split("/").length);
      return patterns;
    }
  } catch {
    // manifest absent (pre-build dev server) — fall through to src/app walk
  }

  // Fallback: walk src/app/ for dev without a completed build.
  // Strips route groups (e.g. /(auth)/, /(dashboard)/) from patterns.
  function walk(dir: string) {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        walk(fullPath);
      } else if (entry.name === "route.ts" || entry.name === "route.js") {
        const routePattern = fullPath
          .replace(appDir, "")
          .replace(/\/route\.(ts|js)$/, "")
          .replace(/\\/g, "/")
          .replace(/\/\([^)]+\)/g, ""); // strip route groups: /(auth) → ""

        const regexStr = routePattern
          .replace(/\[\.\.\.[^\]]+\]/g, "(?:.+)") // [...catchAll] segments
          .replace(/\[[^\]]+\]/g, "(?:[^/]+)"); // [param] segments

        patterns.push([new RegExp(`^${regexStr}(?:\\?.*)?$`), routePattern]);
      }
    }
  }

  try {
    walk(appDir);
    patterns.sort((a, b) => b[1].split("/").length - a[1].split("/").length);
  } catch {
    // src/app not present either — normalisation disabled, raw paths used
  }

  return patterns;
}

const routeMatchers = buildRouteMatchers(path.join(process.cwd(), "src", "app"));

function normalizeRoute(rawUrl: string): string {
  const urlPath = rawUrl.split("?")[0];
  for (const [regex, pattern] of routeMatchers) {
    if (regex.test(urlPath)) return pattern;
  }
  return urlPath;
}

const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;

if (!endpoint) {
  console.warn("[OTel] OTEL_EXPORTER_OTLP_ENDPOINT is not set — tracing disabled");
} else {
  console.log(`[OTel] Starting SDK, exporting to ${endpoint}/v1/traces`);

  const traceExporter = new OTLPTraceExporter({
    url: `${endpoint}/v1/traces`,
  });

  const sdk = new NodeSDK({
    resource: resourceFromAttributes({
      [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME ?? "scopegate",
    }),
    spanProcessors: [
      new SensitiveUrlSpanProcessor(),
      new BatchSpanProcessor(traceExporter),
    ],
    metricReaders: [
      new PeriodicExportingMetricReader({
        exporter: new OTLPMetricExporter({
          url: `${endpoint}/v1/metrics`,
        }),
        exportIntervalMillis: 60_000,
      }),
    ],
    instrumentations: [
      new PrismaInstrumentation(),
      getNodeAutoInstrumentations({
        "@opentelemetry/instrumentation-fs": { enabled: false },
        "@opentelemetry/instrumentation-http": {
          requestHook: (span, request) => {
            // IncomingMessage (server requests) has `url`; ClientRequest does not.
            // Set http.route on incoming server spans so SigNoz can filter by route.
            const req = request as {
              url?: string;
              headers?: Record<string, string | string[] | undefined>;
            };
            if (typeof req.url !== "string" || !req.url) return;

            // Capture client identity on every incoming span so any 400 in SigNoz
            // can be traced back to its source (IP, User-Agent) for diagnostics.
            const rawIp = req.headers?.["x-forwarded-for"];
            const ip = Array.isArray(rawIp) ? rawIp[0] : rawIp;
            const rawUa = req.headers?.["user-agent"];
            const ua = Array.isArray(rawUa) ? rawUa[0] : rawUa;
            if (ip) span.setAttribute("client.address", ip);
            if (ua) span.setAttribute("user_agent.original", ua.slice(0, 256));

            const actionId = req.headers?.["next-action"];
            if (typeof actionId === "string" && !VALID_ACTION_ID.test(actionId)) {
              // Middleware will 400 this — group under a canonical route so SigNoz
              // shows these bot-scan blocks as a single aggregated route, not noise.
              span.setAttribute("http.route", "/[blocked-action]");
              span.setAttribute("block.reason", "invalid-next-action");
              blockedRequestsCounter?.add(1);
            } else {
              span.setAttribute("http.route", normalizeRoute(req.url));
            }
          },
        },
        "@opentelemetry/instrumentation-undici": {
          // Name outgoing fetch spans as "METHOD hostname" (e.g. "GET api.github.com")
          // so anonymous "GET" spans in SigNoz become identifiable by their target host.
          requestHook: (span, request) => {
            try {
              const host = new URL((request as { origin: string }).origin).hostname;
              const method = (request as { method: string }).method;
              const path = (request as { path: string }).path;
              // Strip sensitive query params from all URL attributes before recording.
              const sanitizedPath = redactSensitiveUrlValues(path);
              const sanitizedUrl = `${(request as { origin: string }).origin}${sanitizedPath}`;
              span.updateName(`${method} ${host}`);
              span.setAttribute("peer.service", host);
              span.setAttribute("url.path", sanitizedPath.split("?")[0]);
              span.setAttribute("url.full", sanitizedUrl);
              span.setAttribute("http.url", sanitizedUrl);
            } catch {
              // invalid origin — leave span name unchanged
            }
          },
          ignoreRequestHook: (request) => {
            const fullUrl = `${request.origin}${request.path}`;
            return OAUTH_TOKEN_URL_PATTERNS.some((pattern) =>
              fullUrl.includes(pattern)
            );
          },
        },
      }),
    ],
  });

  sdk.start();

  blockedRequestsCounter = metrics.getMeter("scopegate").createCounter(
    "mcp.blocked_requests",
    { description: "Requests blocked by middleware due to invalid Next-Action header" },
  );
}
