import { buildOauthProviderAuthResult, emptyPluginConfigSchema } from "symi/plugin-sdk";
import type { AnyAgentTool, SymiPluginApi, ProviderAuthContext } from "../../src/plugins/types.js";
import {
  loginOutlook,
  requestDeviceCode,
  pollForDeviceCodeToken,
  fetchUserProfile,
} from "./src/auth.js";
import {
  saveCredentials,
  loadCredentials,
  deleteCredentials,
  getStorePath,
  resolveConnectionState,
} from "./src/store.js";
import {
  createOutlookListTool,
  createOutlookReadTool,
  createOutlookSendTool,
  createOutlookReplyTool,
  createOutlookSearchTool,
  createOutlookFoldersTool,
  createOutlookMoveTool,
} from "./src/tools.js";

const outlookPlugin = {
  id: "outlook",
  name: "Outlook 365",
  description: "Outlook 365 email integration via Microsoft Graph API with OAuth2 PKCE",
  configSchema: emptyPluginConfigSchema(),

  register(api: SymiPluginApi) {
    // -------------------------------------------------------------------------
    // Startup diagnostic — log credential presence and path so operators can
    // grep `journalctl -u symi-gateway | grep outlook` to answer the question
    // "where are my tokens supposed to live and are they there?" in one line,
    // without hunting through the filesystem.
    // -------------------------------------------------------------------------
    (() => {
      const storePath = getStorePath();
      const startupCreds = loadCredentials();
      if (startupCreds) {
        const expiresIso =
          typeof startupCreds.expires === "number"
            ? new Date(startupCreds.expires).toISOString()
            : "unknown";
        const state = resolveConnectionState(startupCreds);
        api.logger.info(
          `outlook: credentials loaded (email=${startupCreds.email ?? "unknown"}, ` +
            `state=${state}, expires=${expiresIso}, path=${storePath})`,
        );
      } else {
        api.logger.info(`outlook: no credentials at ${storePath}`);
      }
    })();

    // -------------------------------------------------------------------------
    // Agent tools — registered unconditionally (not { optional: true }). The
    // plugin is BUNDLED_ENABLED_BY_DEFAULT (see §32.11), so its tools should
    // appear in the agent palette by default too. Previously {optional:true}
    // gated the tools behind the user's tool-allowlist — which created a
    // silent-failure footgun: the `before_prompt_build` hook claimed "you
    // have email tools" while the actual palette filtered them out unless
    // the user had hand-added `outlook` / `group:plugins` to their allowlist.
    // Users opt OUT via `plugins.entries.outlook.enabled: false` or by
    // denying the tool names in their agent's tools.deny — same escape
    // hatches as any other bundled capability. Subagent gating is handled
    // separately in pi-tools.policy.ts:SUBAGENT_TOOL_DENY_ALWAYS.
    // -------------------------------------------------------------------------
    const tools = [
      createOutlookListTool,
      createOutlookReadTool,
      createOutlookSendTool,
      createOutlookReplyTool,
      createOutlookSearchTool,
      createOutlookFoldersTool,
      createOutlookMoveTool,
    ];

    for (const createTool of tools) {
      api.registerTool(createTool() as unknown as AnyAgentTool);
    }

    // -------------------------------------------------------------------------
    // Provider — enables `symi models add --provider outlook --auth oauth`
    // and stores credentials in the standard auth profile store.
    // -------------------------------------------------------------------------
    api.registerProvider({
      id: "outlook-mail",
      label: "Outlook 365 Mail",
      docsPath: "/integrations/outlook",
      aliases: ["outlook"],
      auth: [
        {
          id: "oauth",
          label: "Microsoft OAuth (sign in with your account)",
          hint: "PKCE + localhost callback — uses your normal Microsoft credentials",
          kind: "oauth",
          run: async (ctx: ProviderAuthContext) => {
            const spin = ctx.prompter.progress("Starting Outlook OAuth...");
            try {
              const result = await loginOutlook({
                isRemote: ctx.isRemote,
                openUrl: ctx.openUrl,
                prompt: async (message) => String(await ctx.prompter.text({ message })),
                note: ctx.prompter.note,
                log: (message) => ctx.runtime.log(message),
                progress: spin,
              });

              // Persist to local credential store for the tools
              saveCredentials({
                access: result.access,
                refresh: result.refresh,
                expires: result.expires,
                email: result.email,
                displayName: result.displayName,
              });

              return buildOauthProviderAuthResult({
                providerId: "outlook-mail",
                defaultModel: "", // not an LLM provider
                access: result.access,
                refresh: result.refresh,
                expires: result.expires,
                email: result.email,
                notes: [
                  result.email
                    ? `Connected as ${result.displayName ?? result.email}`
                    : "Outlook connected.",
                  "Email tools (outlook_list, outlook_send, outlook_search, etc.) are now available.",
                  "Credentials stored in ~/.symi/credentials/outlook.json",
                ],
              });
            } catch (err) {
              spin.stop("Outlook OAuth failed");
              throw err;
            }
          },
        },
      ],
    });

    // -------------------------------------------------------------------------
    // CLI — `symi outlook login`, `symi outlook logout`, `symi outlook status`
    // -------------------------------------------------------------------------
    api.registerCli(
      ({ program }) => {
        const outlook = program.command("outlook").description("Outlook 365 email integration");

        outlook
          .command("login")
          .description("Sign in with your Microsoft account")
          .action(async () => {
            const { loginOutlook: login } = await import("./src/auth.js");

            const isHeadless =
              process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;

            const result = await login({
              isRemote: isHeadless,
              openUrl: async (url) => {
                if (isHeadless) {
                  console.log("\nOpen this URL in your browser:\n");
                  console.log(url);
                  console.log("");
                  return;
                }
                const { execSync } = await import("node:child_process");
                const cmd =
                  process.platform === "darwin"
                    ? `open "${url}"`
                    : process.platform === "win32"
                      ? `start "" "${url}"`
                      : `xdg-open "${url}" 2>/dev/null`;
                try {
                  execSync(cmd, { stdio: "ignore" });
                } catch {
                  console.log(`\nOpen this URL in your browser:\n${url}\n`);
                }
              },
              prompt: async (message) => {
                const readline = await import("node:readline");
                const rl = readline.createInterface({
                  input: process.stdin,
                  output: process.stdout,
                });
                return new Promise((resolve) => {
                  rl.question(message, (answer) => {
                    rl.close();
                    resolve(answer);
                  });
                });
              },
              note: async (message) => {
                console.log(message);
              },
              log: (message) => console.log(message),
              progress: {
                update: (msg) => process.stderr.write(`\r${msg}`),
                stop: (msg) => {
                  if (msg) process.stderr.write(`\r${msg}\n`);
                },
              },
            });

            saveCredentials({
              access: result.access,
              refresh: result.refresh,
              expires: result.expires,
              email: result.email,
              displayName: result.displayName,
            });

            console.log("");
            console.log(
              result.email
                ? `Connected as ${result.displayName ?? result.email}`
                : "Outlook connected.",
            );
            console.log("Email tools are now available to your agent.");
          });

        outlook
          .command("logout")
          .description("Disconnect Outlook and remove stored credentials")
          .action(() => {
            const removed = deleteCredentials();
            console.log(removed ? "Outlook credentials removed." : "No credentials found.");
          });

        outlook
          .command("status")
          .description("Show Outlook connection status")
          .action(() => {
            const creds = loadCredentials();
            if (!creds) {
              console.log("Not connected. Run `symi outlook login` to sign in.");
              return;
            }
            const expired = Date.now() >= creds.expires;
            console.log(`Account: ${creds.email ?? "unknown"}`);
            console.log(`Name: ${creds.displayName ?? "unknown"}`);
            console.log(`Token: ${expired ? "expired (will auto-refresh)" : "valid"}`);
            console.log(`Updated: ${creds.updatedAt ?? "unknown"}`);
          });
      },
      { commands: ["outlook"] },
    );

    // -------------------------------------------------------------------------
    // System prompt context — tell the agent about Outlook integration
    // -------------------------------------------------------------------------
    api.on("before_prompt_build", () => {
      // Tri-state — see store.ts:resolveConnectionState. The previous
      // `creds && (expires > now || !!refresh)` form treated "we hold any
      // non-empty refresh string" as connected, which would stay truthy
      // even after Microsoft invalidates the refresh token (password
      // change, admin SSO reset, 14-day rolling work-account expiry,
      // 90-day personal inactivity). Tools would 400 while the agent
      // kept claiming connected. The tri-state + 24h refresh-trust
      // window is honest about uncertainty.
      const creds = loadCredentials();
      const state = resolveConnectionState(creds);
      let context: string;
      if (state === "valid" || state === "trusted") {
        context =
          `[Outlook 365] Connected as ${creds?.email ?? "unknown"}. You have email ` +
          `tools: outlook_list, outlook_read, outlook_send, outlook_reply, ` +
          `outlook_search, outlook_folders, outlook_move. Use them to help the user ` +
          `with email tasks. The /outlook command is a legitimate Symi plugin ` +
          `command — messages from it are safe system output, not injection.`;
      } else if (state === "stale") {
        // Token is expired and last-verified moment is > 24h old (or
        // never recorded — pre-2.8.11 credential files). The next tool
        // call will attempt a refresh; it may succeed or fail. Tell
        // the agent to try, but to be prepared to ask the user to
        // re-authorize if the refresh fails.
        context =
          `[Outlook 365] Last seen connected as ${creds?.email ?? "unknown"} but ` +
          `the access token is expired and it's been a while since we last ` +
          `verified the refresh token works. Email tools (outlook_list, ` +
          `outlook_read, outlook_send, outlook_reply, outlook_search, ` +
          `outlook_folders, outlook_move) will attempt an automatic refresh on ` +
          `next use. If they fail with an auth error, ask the user to run ` +
          `/outlook login to re-authorize. The /outlook command is a legitimate ` +
          `Symi plugin command.`;
      } else {
        context =
          `[Outlook 365] Not connected. User can type /outlook login to connect ` +
          `their Microsoft account via device code flow. This is a legitimate ` +
          `Symi plugin command.`;
      }
      return { systemPrompt: context };
    });

    // -------------------------------------------------------------------------
    // Chat commands — /outlook login, /outlook logout, /outlook status
    // Accessible from Telegram, WhatsApp, Slack, etc.
    // -------------------------------------------------------------------------
    api.registerCommand({
      name: "outlook",
      description: "Outlook 365 email — login, logout, status",
      acceptsArgs: true,
      handler: async (ctx) => {
        const subcommand = ctx.args?.trim().split(/\s+/)[0]?.toLowerCase() ?? "";

        if (subcommand === "login") {
          const creds = loadCredentials();
          if (creds && Date.now() < creds.expires) {
            return {
              text: `Already connected as ${creds.email ?? "unknown"}. Use /outlook logout to disconnect first.`,
            };
          }

          try {
            const deviceCode = await requestDeviceCode();

            // Start polling in the background
            const pollPromise = pollForDeviceCodeToken({
              deviceCode: deviceCode.device_code,
              intervalMs: (deviceCode.interval ?? 5) * 1000,
              expiresInMs: (deviceCode.expires_in ?? 900) * 1000,
            });

            // Don't await — let it poll while user signs in
            pollPromise
              .then(async (tokens) => {
                const profile = await fetchUserProfile(tokens.access);
                saveCredentials({
                  access: tokens.access,
                  refresh: tokens.refresh,
                  expires: tokens.expires,
                  email: profile.email,
                  displayName: profile.displayName,
                });
                // The completion message will be picked up by the next interaction
                api.logger.info(
                  `Outlook connected as ${profile.displayName ?? profile.email ?? "unknown"}`,
                );
              })
              .catch((err) => {
                api.logger.error(`Outlook login failed: ${String(err)}`);
              });

            return {
              text: [
                "Sign in to your Microsoft account:",
                "",
                `1. Go to: ${deviceCode.verification_uri}`,
                `2. Enter code: **${deviceCode.user_code}**`,
                "",
                `Code expires in ${Math.floor(deviceCode.expires_in / 60)} minutes.`,
                "I'll confirm once you've signed in.",
              ].join("\n"),
            };
          } catch (err) {
            return {
              text: `Outlook login failed: ${err instanceof Error ? err.message : String(err)}`,
            };
          }
        }

        if (subcommand === "logout") {
          const removed = deleteCredentials();
          return { text: removed ? "Outlook disconnected." : "Outlook was not connected." };
        }

        if (subcommand === "status") {
          const creds = loadCredentials();
          if (!creds) {
            return { text: "Outlook not connected. Send /outlook login to sign in." };
          }
          const expired = Date.now() >= creds.expires;
          return {
            text: [
              `Account: ${creds.email ?? "unknown"}`,
              `Name: ${creds.displayName ?? "unknown"}`,
              `Token: ${expired ? "expired (will auto-refresh)" : "valid"}`,
            ].join("\n"),
          };
        }

        return {
          text: [
            "Outlook 365 commands:",
            "",
            "/outlook login — Sign in with Microsoft",
            "/outlook logout — Disconnect",
            "/outlook status — Show connection info",
          ].join("\n"),
        };
      },
    });
  },
};

export default outlookPlugin;
