import { createHash, randomBytes } from "node:crypto";
import { createServer } from "node:http";
import { isWSL2Sync } from "symi/plugin-sdk";

// Microsoft OAuth 2.0 endpoints (multi-tenant: any Microsoft account)
const AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
const TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
const REDIRECT_URI = "http://localhost:51122/oauth-callback";

// Scopes for Outlook mail access + offline refresh
const SCOPES = [
  "https://graph.microsoft.com/Mail.Read",
  "https://graph.microsoft.com/Mail.Send",
  "https://graph.microsoft.com/Mail.ReadWrite",
  "https://graph.microsoft.com/User.Read",
  "offline_access",
];

const RESPONSE_PAGE = `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Symi Outlook OAuth</title>
    <style>
      body { font-family: system-ui, sans-serif; display: flex; justify-content: center;
             align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
      main { text-align: center; padding: 2rem; background: white;
             border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
      h1 { color: #0078d4; }
    </style>
  </head>
  <body>
    <main>
      <h1>Outlook connected</h1>
      <p>You can return to the terminal.</p>
    </main>
  </body>
</html>`;

// Symi's registered multi-tenant Azure AD app (public client, no secret needed).
// Users can override with SYMI_OUTLOOK_CLIENT_ID if they have their own app registration.
const DEFAULT_CLIENT_ID = "e24f680b-3fdd-4bce-a201-6bf547fe4735";

function resolveClientId(): string {
  return process.env.SYMI_OUTLOOK_CLIENT_ID?.trim() || DEFAULT_CLIENT_ID;
}

export function generatePkce(): { verifier: string; challenge: string } {
  const verifier = randomBytes(32).toString("hex");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  return { verifier, challenge };
}

export function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
  if (isRemote || isWSL2Sync()) {
    return true;
  }
  // Headless Linux (no display server) — can't open a browser
  if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
    return true;
  }
  return false;
}

export function buildAuthUrl(params: { challenge: string; state: string }): string {
  const clientId = resolveClientId();
  const url = new URL(AUTH_URL);
  url.searchParams.set("client_id", clientId);
  url.searchParams.set("response_type", "code");
  url.searchParams.set("redirect_uri", REDIRECT_URI);
  url.searchParams.set("scope", SCOPES.join(" "));
  url.searchParams.set("code_challenge", params.challenge);
  url.searchParams.set("code_challenge_method", "S256");
  url.searchParams.set("state", params.state);
  url.searchParams.set("prompt", "consent");
  return url.toString();
}

export function parseCallbackInput(
  input: string,
): { code: string; state: string } | { error: string } {
  const trimmed = input.trim();
  if (!trimmed) {
    return { error: "No input provided" };
  }
  try {
    const url = new URL(trimmed);
    const code = url.searchParams.get("code");
    const state = url.searchParams.get("state");
    if (!code) {
      return { error: "Missing 'code' parameter in URL" };
    }
    if (!state) {
      return { error: "Missing 'state' parameter in URL" };
    }
    return { code, state };
  } catch {
    return { error: "Paste the full redirect URL (not just the code)." };
  }
}

export async function startCallbackServer(params: { timeoutMs: number }) {
  const redirect = new URL(REDIRECT_URI);
  const port = redirect.port ? Number(redirect.port) : 51122;

  let settled = false;
  let resolveCallback: (url: URL) => void;
  let rejectCallback: (err: Error) => void;

  const callbackPromise = new Promise<URL>((resolve, reject) => {
    resolveCallback = (url) => {
      if (settled) return;
      settled = true;
      resolve(url);
    };
    rejectCallback = (err) => {
      if (settled) return;
      settled = true;
      reject(err);
    };
  });

  const timeout = setTimeout(() => {
    rejectCallback(new Error("Timed out waiting for OAuth callback"));
  }, params.timeoutMs);
  timeout.unref?.();

  const server = createServer((request, response) => {
    if (!request.url) {
      response.writeHead(400, { "Content-Type": "text/plain" });
      response.end("Missing URL");
      return;
    }

    const url = new URL(request.url, `${redirect.protocol}//${redirect.host}`);
    if (url.pathname !== redirect.pathname) {
      response.writeHead(404, { "Content-Type": "text/plain" });
      response.end("Not found");
      return;
    }

    response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
    response.end(RESPONSE_PAGE);
    resolveCallback(url);

    setImmediate(() => {
      server.close();
    });
  });

  await new Promise<void>((resolve, reject) => {
    const onError = (err: Error) => {
      server.off("error", onError);
      reject(err);
    };
    server.once("error", onError);
    server.listen(port, "127.0.0.1", () => {
      server.off("error", onError);
      resolve();
    });
  });

  return {
    waitForCallback: () => callbackPromise,
    close: () =>
      new Promise<void>((resolve) => {
        server.close(() => resolve());
      }),
  };
}

export async function exchangeCode(params: {
  code: string;
  verifier: string;
}): Promise<{ access: string; refresh: string; expires: number }> {
  const clientId = resolveClientId();
  const response = await fetch(TOKEN_URL, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      client_id: clientId,
      code: params.code,
      grant_type: "authorization_code",
      redirect_uri: REDIRECT_URI,
      code_verifier: params.verifier,
    }),
  });

  if (!response.ok) {
    const text = await response.text();
    throw new Error(`Token exchange failed: ${text}`);
  }

  const data = (await response.json()) as {
    access_token?: string;
    refresh_token?: string;
    expires_in?: number;
  };

  const access = data.access_token?.trim();
  const refresh = data.refresh_token?.trim();
  const expiresIn = data.expires_in ?? 0;

  if (!access) {
    throw new Error("Token exchange returned no access_token");
  }
  if (!refresh) {
    throw new Error("Token exchange returned no refresh_token (ensure offline_access scope)");
  }

  // 5-minute buffer before actual expiry (same as google-antigravity)
  const expires = Date.now() + expiresIn * 1000 - 5 * 60 * 1000;
  return { access, refresh, expires };
}

export async function refreshAccessToken(refreshToken: string): Promise<{
  access: string;
  refresh: string;
  expires: number;
}> {
  const clientId = resolveClientId();
  const response = await fetch(TOKEN_URL, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      client_id: clientId,
      grant_type: "refresh_token",
      refresh_token: refreshToken,
      scope: SCOPES.join(" "),
    }),
  });

  if (!response.ok) {
    const text = await response.text();
    throw new Error(`Token refresh failed: ${text}`);
  }

  const data = (await response.json()) as {
    access_token?: string;
    refresh_token?: string;
    expires_in?: number;
  };

  const access = data.access_token?.trim();
  // Microsoft may or may not rotate the refresh token
  const refresh = data.refresh_token?.trim() ?? refreshToken;
  const expiresIn = data.expires_in ?? 0;

  if (!access) {
    throw new Error("Token refresh returned no access_token");
  }

  const expires = Date.now() + expiresIn * 1000 - 5 * 60 * 1000;
  return { access, refresh, expires };
}

// =========================================================================
// Device Code Flow — for chat-based login (no browser on this device)
// =========================================================================

const DEVICE_CODE_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode";

export type DeviceCodeResponse = {
  device_code: string;
  user_code: string;
  verification_uri: string;
  expires_in: number;
  interval: number;
};

export async function requestDeviceCode(): Promise<DeviceCodeResponse> {
  const clientId = resolveClientId();
  const response = await fetch(DEVICE_CODE_URL, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      client_id: clientId,
      scope: SCOPES.join(" "),
    }),
  });

  if (!response.ok) {
    const text = await response.text();
    throw new Error(`Device code request failed: ${text}`);
  }

  return (await response.json()) as DeviceCodeResponse;
}

export async function pollForDeviceCodeToken(params: {
  deviceCode: string;
  intervalMs: number;
  expiresInMs: number;
}): Promise<{ access: string; refresh: string; expires: number }> {
  const clientId = resolveClientId();
  const deadline = Date.now() + params.expiresInMs;

  while (Date.now() < deadline) {
    await new Promise((resolve) => setTimeout(resolve, params.intervalMs));

    const response = await fetch(TOKEN_URL, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: clientId,
        grant_type: "urn:ietf:params:oauth:grant-type:device_code",
        device_code: params.deviceCode,
      }),
    });

    const data = (await response.json()) as {
      access_token?: string;
      refresh_token?: string;
      expires_in?: number;
      error?: string;
    };

    if (data.error === "authorization_pending") {
      continue;
    }
    if (data.error === "slow_down") {
      // Back off by adding 5 seconds
      params.intervalMs += 5000;
      continue;
    }
    if (data.error) {
      throw new Error(`Device code auth failed: ${data.error}`);
    }

    const access = data.access_token?.trim();
    const refresh = data.refresh_token?.trim();
    const expiresIn = data.expires_in ?? 0;

    if (!access) {
      throw new Error("Token exchange returned no access_token");
    }
    if (!refresh) {
      throw new Error("Token exchange returned no refresh_token");
    }

    const expires = Date.now() + expiresIn * 1000 - 5 * 60 * 1000;
    return { access, refresh, expires };
  }

  throw new Error("Device code expired. Please try again.");
}

export async function fetchUserProfile(accessToken: string): Promise<{
  email?: string;
  displayName?: string;
}> {
  try {
    const response = await fetch("https://graph.microsoft.com/v1.0/me", {
      headers: { Authorization: `Bearer ${accessToken}` },
    });
    if (!response.ok) {
      return {};
    }
    const data = (await response.json()) as {
      mail?: string;
      userPrincipalName?: string;
      displayName?: string;
    };
    return {
      email: data.mail || data.userPrincipalName,
      displayName: data.displayName,
    };
  } catch {
    return {};
  }
}

export async function loginOutlook(params: {
  isRemote: boolean;
  openUrl: (url: string) => Promise<void>;
  prompt: (message: string) => Promise<string>;
  note: (message: string, title?: string) => Promise<void>;
  log: (message: string) => void;
  progress: { update: (msg: string) => void; stop: (msg?: string) => void };
}): Promise<{
  access: string;
  refresh: string;
  expires: number;
  email?: string;
  displayName?: string;
}> {
  const { verifier, challenge } = generatePkce();
  const state = randomBytes(16).toString("hex");
  const authUrl = buildAuthUrl({ challenge, state });

  let callbackServer: Awaited<ReturnType<typeof startCallbackServer>> | null = null;
  const needsManual = shouldUseManualOAuthFlow(params.isRemote);
  if (!needsManual) {
    try {
      callbackServer = await startCallbackServer({ timeoutMs: 5 * 60 * 1000 });
    } catch {
      callbackServer = null;
    }
  }

  if (!callbackServer) {
    await params.note(
      [
        "Open the URL in your local browser.",
        "After signing in, copy the full redirect URL and paste it back here.",
        "",
        `Auth URL: ${authUrl}`,
        `Redirect URI: ${REDIRECT_URI}`,
      ].join("\n"),
      "Outlook OAuth",
    );
    params.log("");
    params.log("Copy this URL:");
    params.log(authUrl);
    params.log("");
  }

  if (!needsManual) {
    params.progress.update("Opening Microsoft sign-in...");
    try {
      await params.openUrl(authUrl);
    } catch {
      // ignore
    }
  }

  let code = "";
  let returnedState = "";

  if (callbackServer) {
    params.progress.update("Waiting for OAuth callback...");
    const callback = await callbackServer.waitForCallback();
    code = callback.searchParams.get("code") ?? "";
    returnedState = callback.searchParams.get("state") ?? "";
    await callbackServer.close();
  } else {
    params.progress.update("Waiting for redirect URL...");
    const input = await params.prompt("Paste the redirect URL: ");
    const parsed = parseCallbackInput(input);
    if ("error" in parsed) {
      throw new Error(parsed.error);
    }
    code = parsed.code;
    returnedState = parsed.state;
  }

  if (!code) {
    throw new Error("Missing OAuth code");
  }
  if (returnedState !== state) {
    throw new Error("OAuth state mismatch. Please try again.");
  }

  params.progress.update("Exchanging code for tokens...");
  const tokens = await exchangeCode({ code, verifier });
  const profile = await fetchUserProfile(tokens.access);

  params.progress.stop("Outlook OAuth complete");
  return { ...tokens, ...profile };
}
