import express, { Application, Request, Response, NextFunction } from "express";
import cors from "cors";
import { ApolloServer } from "apollo-server-express";
import jwt, { JwtPayload } from "jsonwebtoken";
import dotenv from "dotenv";
import { graphqlUploadExpress } from "graphql-upload-minimal";
import userTypeDefs from "./schema/userSchema";
import userResolvers from "./resolver/userResolver";
import { UserModel } from "./models/userModel";
import assetResolver from "./resolver/assetRsolver";
import assetTypeDefs from "./schema/assetSchema";
import { AssetModel } from "./models/assetModel";
import oilPositionUserTypeDefs from "./schema/oilPositionUserSchema";
import oilPositionUserResolvers from "./resolver/oilPositionUserResolver";
import { OilPositionUserModel } from "./models/oilPositionUserModel";
import goldPositionUserTypeDefs from "./schema/goldPositionUserSchema";
import goldPositionUserResolvers from "./resolver/goldPositionUserResolver";
import { GoldPositionUserModel } from "./models/goldPositionUserModel";
import silverPositionUserTypeDefs from "./schema/silverPositionUserSchema";
import silverPositionUserResolvers from "./resolver/silverPositionUserResolver";
import { SilverPositionUserModel } from "./models/silverPositionUserModel";
import copperPositionUserTypeDefs from "./schema/copperPositionUserSchema";
import copperPositionUserResolvers from "./resolver/copperPositionUserResolver";
import { CopperPositionUserModel } from "./models/copperPositionUserModel";
import { createHmac, timingSafeEqual } from "crypto";

dotenv.config({ path: "../.env" });

interface CustomRequest extends Request {
  user?: JwtPayload | any;
}

const JWT_SECRET = process.env.JWT_SECRET as string;
if (!JWT_SECRET) {
  console.error("JWT_SECRET is missing");
  process.exit(1);
}

const PORT = Number(process.env.BE_PORT) || 4000;

const DIDIT_API_KEY = process.env.REACT_APP_API_KEY || "";
const DIDIT_BASE_URL = (process.env.REACT_APP_NEXT_VERIFICATION_BASE_URL || "https://verification.didit.me").replace(/\/+$/, "");
const VERIFICATION_WORKFLOW_ID = process.env.REACT_APP_VERIFICATION_WORKFLOW_ID || "";
const VERIFICATION_CALLBACK_URL = process.env.REACT_APP_VERIFICATION_CALLBACK_URL || "";
const DIDIT_WEBHOOK_URL = process.env.REACT_APP_DIDIT_WEBHOOK_URL || VERIFICATION_CALLBACK_URL || "";


const authenticateJWT = (
  req: CustomRequest,
  res: Response,
  next: NextFunction
) => {
  if (req.url !== "/graphql") {
    return next();
  }

  const query = (req.body?.query || "").toString();
  const normalizedQuery = query.replace(/\s+/g, " ").trim().toLowerCase();

  const publicOperations = [
    "introspectionquery",
    "mutation registeruser",
    "mutation loginuser",
    "mutation forgotpassword",
    "mutation changepassword",
    "mutation updateprofile",
    "mutation emailverification",
    "mutation emailapproval",
    "query getuserbytoken",
  ];

  if (
    req.method === "GET" ||
    publicOperations.some((op) => normalizedQuery.includes(op))
  ) {
    return next();
  }

  const authHeader = req.headers.authorization;
  const token =
    authHeader && authHeader.startsWith("Bearer ")
      ? authHeader.split(" ")[1]
      : null;

  if (!token) {
    return res.status(401).json({ error: "Token is required for this operation" });
  }

  try {
    const user = jwt.verify(token, JWT_SECRET);
    req.user = user;
    next();
  } catch (err) {
    console.error("Token verification error:", err);
    return res.status(403).json({ error: "Invalid or expired token" });
  }
};

const startServer = async () => {
  const app: Application = express();
  app.use("/api/webhooks/didit", express.raw({ type: ["application/json", "application/*+json"] }));
  // Support alternate path used in env: /api/didit/webhooks
  app.use("/api/didit/webhooks", express.raw({ type: ["application/json", "application/*+json"] }));

  // General middleware for the rest of the app
  app.use(express.json({ limit: "10mb" }));
  app.use(express.urlencoded({ extended: true }));
  app.use(cors());
  app.use("/graphql", (req: Request, res: Response, next: NextFunction) => {
    const contentType = (req.headers["content-type"] || "").toString();
    if (contentType.includes("multipart/form-data")) {
      return graphqlUploadExpress()(req, res, next);
    }
    return next();
  });
  app.use(authenticateJWT);

  app.get("/", (_req, res) => res.send("OK"));
  app.get("/api/didit/webhooks", (_req, res) => res.status(204).end());
  app.get("/api/webhooks/didit", (_req, res) => res.status(204).end());
app.post("/api/didit/verification-sessions", async (req: Request, res: Response) => {
  try {
    const DIDIT_API_KEY = process.env.REACT_APP_API_KEY || "";
    const workflowId = process.env.REACT_APP_VERIFICATION_WORKFLOW_ID || "";
    const bodyRedirect = (req.body && (req.body.redirectUri || req.body.redirectUrl || req.body.redirect_url)) || "";
    const bodyCallback = (req.body && req.body.callback) || ""; // only honor explicit callback from client

    if (!workflowId) return res.status(400).json({ error: "workflow_id is required" });
    if (!DIDIT_API_KEY) return res.status(500).json({ error: "Missing DIDIT_API_KEY" });

    const emailFromBody = (req.body && (req.body.email || req.body.vendor_data)) || undefined;
    const payload: Record<string, any> = { workflow_id: workflowId };
    // Do NOT inject callback from server env to avoid browser redirect to webhook URL
    if (bodyCallback) payload.callback = bodyCallback; // only when client explicitly requests
    if (bodyRedirect) payload.redirect_url = bodyRedirect; // user/browser redirect
    if (emailFromBody) payload.vendor_data = emailFromBody; 

    const r = await fetch("https://verification.didit.me/v2/session/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": DIDIT_API_KEY, 
      },
      body: JSON.stringify(payload),
    });

    const text = await r.text();
    let json: any = null;
    try { json = JSON.parse(text || "{}"); } catch {}

    if (!r.ok) {
      return res.status(r.status).json({ error: "Failed to create verification session", raw: json || text });
    }

    if (json?.session_id && emailFromBody) {
      const user = await UserModel.findByEmail(emailFromBody);
      if (user) {
        await UserModel.upsertKycSession({
          userId: user.id,
          sessionId: String(json.session_id),
          status: "Pending",
          provider: "didit",
          raw: json,
        });
      }
    }

    return res.status(r.status).json(json || text);
  } catch (e) {
    console.error("Didit session error:", e);
    return res.status(500).json({ error: "Internal error" });
  }
});

  // Shared Didit webhook handler (mounted on two routes below)
  const diditWebhookHandler = async (req: Request, res: Response) => {
    try {
      // Raw body is required for signature verification
      const rawBodyBuf = req.body as Buffer;
      const rawBody = Buffer.isBuffer(rawBodyBuf) ? rawBodyBuf : Buffer.from(rawBodyBuf || "");
      const contentType = String(req.headers["content-type"] || "");
      const sigPresent = Boolean(
        (req.headers["x-didit-signature"] as string | undefined) ||
        (req.headers["x-signature"] as string | undefined) ||
        (req.headers["x-hub-signature-256"] as string | undefined) ||
        (req.headers["x-webhook-signature"] as string | undefined)
      );

      // Optional HMAC verification if secret and signature header provided
      const secret = process.env.REACT_APP_WEBHOOK_SECRET_KEY || process.env.REACT_APP_DIDIT_WEBHOOK_SECRET_KEY || "";
      const sigHeader =
        (req.headers["x-didit-signature"] as string | undefined) ||
        (req.headers["x-signature"] as string | undefined) ||
        (req.headers["x-hub-signature-256"] as string | undefined) ||
        (req.headers["x-webhook-signature"] as string | undefined);

      if (secret && sigHeader) {
        const h = createHmac("sha256", secret).update(rawBody).digest();
        const hex = Buffer.from(h).toString("hex");
        const b64 = Buffer.from(h).toString("base64");
        const prefixed = `sha256=${hex}`;

        const candidate = Buffer.from(sigHeader);
        const expectedHex = Buffer.from(hex);
        const expectedB64 = Buffer.from(b64);
        const expectedPrefixed = Buffer.from(prefixed);

        const equal =
          (candidate.length === expectedHex.length && timingSafeEqual(candidate, expectedHex)) ||
          (candidate.length === expectedB64.length && timingSafeEqual(candidate, expectedB64)) ||
          (candidate.length === expectedPrefixed.length && timingSafeEqual(candidate, expectedPrefixed));

        if (!equal) {
          console.warn("Didit webhook signature mismatch");
          return res.status(400).send("Invalid signature");
        }
      }
      else {
        console.warn("Didit webhook: no signature to verify");
      }

      // Parse JSON
      let payload: any = {};
      try {
        payload = JSON.parse(rawBody.toString("utf8") || "{}");
      } catch (e) {
        console.error("Didit webhook parse error", e);
        return res.status(400).send("Invalid JSON");
      }

      // Extract session ID from a variety of shapes
      const sessionId =
        payload.session_id ||
        payload.sessionId ||
        payload?.session?.id ||
        payload?.session?.session_id ||
        payload?.data?.session_id ||
        payload?.data?.id ||
        payload?.id;

      // Extract status from a variety of fields
      const rawStatus: string = (
        payload.status ||
        payload.result ||
        payload.verification_status ||
        payload?.outcome?.status ||
        payload?.data?.status ||
        payload?.event ||
        ""
      )
        .toString()
        .toLowerCase();

      const mapStatus = (s: string): "Pending" | "Approved" | "Rejected" | "Cancelled" => {
        if (!s) return "Pending";
        if (["approved", "verified", "success", "succeeded", "completed", "pass", "passed"].includes(s)) return "Approved";
        if (["rejected", "declined", "failed", "fail", "denied"].includes(s)) return "Rejected";
        if (["cancelled", "canceled", "aborted", "void"].includes(s)) return "Cancelled";
        return "Pending";
      };

      const status = mapStatus(rawStatus);

      try {
        const payloadKeys = payload && typeof payload === "object" ? Object.keys(payload) : [];
        console.log("Didit webhook: parsed", { sessionId, rawStatus, status, payloadKeys: payloadKeys.slice(0, 20) });
      } catch {}

      if (!sessionId) {
        console.warn("Didit webhook missing session id", { rawStatus, payloadKeys: Object.keys(payload || {}) });
        // Acknowledge to avoid retries if provider doesn’t guarantee id here
        return res.status(200).json({ ok: true });
      }

      await UserModel.updateKycStatus(String(sessionId), status, payload);
      return res.status(200).json({ ok: true });
    } catch (e) {
      console.error("Didit webhook error", e);
      return res.status(500).send("Server error");
    }
  };

  // Mount webhook on both routes to be flexible with configured URL
  app.post("/api/webhooks/didit", diditWebhookHandler);
  app.post("/api/didit/webhooks", diditWebhookHandler);

  app.get("/api/kyc/status", async (req: Request, res: Response) => {
    try {
      const sessionId = (req.query.session_id || req.query.sessionId || "").toString().trim();
      if (!sessionId) return res.status(400).json({ error: "session_id is required" });
      const row = await UserModel.findKycBySessionId(sessionId);
      if (!row) return res.status(200).json({ sessionId, status: "Pending" });
      return res.status(200).json({ sessionId, status: row.status, updatedAt: row.updatedAt, userId: row.userId });
    } catch (e) {
      console.error("KYC status lookup error", e);
      return res.status(500).json({ error: "Server error" });
    }
  });


  const combineTypeDefs = [
    userTypeDefs,
    assetTypeDefs,
    oilPositionUserTypeDefs,
    goldPositionUserTypeDefs,
    silverPositionUserTypeDefs,
    copperPositionUserTypeDefs,
  ];
  const combineResolvers = [
    userResolvers,
    assetResolver,
    oilPositionUserResolvers,
    goldPositionUserResolvers,
    silverPositionUserResolvers,
    copperPositionUserResolvers,
  ];

  const server = new ApolloServer({
    typeDefs: combineTypeDefs,
    resolvers: combineResolvers,
    context: ({ req }: { req: CustomRequest }) => ({
      UserModel,
      AssetModel,
      OilPositionUserModel,
      GoldPositionUserModel,
      SilverPositionUserModel,
      CopperPositionUserModel,
      user: req.user,
    }),
  });

  await server.start();
  server.applyMiddleware({ app });

  app.listen(PORT, () => {
    console.info(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
    console.info(`📫 POST Didit sessions at http://localhost:${PORT}/api/didit/verification-sessions`);
    console.info(`🔐 Using Bearer auth against: ${DIDIT_BASE_URL}`);
    if (DIDIT_WEBHOOK_URL) console.info(`🪝 Didit webhook URL configured: ${DIDIT_WEBHOOK_URL}`);
  });
};

startServer().catch((err) => {
  console.error("Failed to start server:", err);
  process.exit(1);
});
