import {
  buildAccountScopedDmSecurityPolicy,
  createAccountStatusSink,
  mapAllowFromEntries,
} from "openclaw/plugin-sdk/compat";
import type {
  ChannelAccountSnapshot,
  ChannelDirectoryEntry,
  ChannelDock,
  ChannelGroupContext,
  ChannelMessageActionAdapter,
  ChannelPlugin,
  OpenClawConfig,
  GroupToolPolicyConfig,
} from "openclaw/plugin-sdk/zalouser";
import {
  applyAccountNameToChannelSection,
  applySetupAccountConfigPatch,
  buildChannelSendResult,
  buildBaseAccountStatusSnapshot,
  buildChannelConfigSchema,
  DEFAULT_ACCOUNT_ID,
  deleteAccountFromConfigSection,
  formatAllowFromLowercase,
  isDangerousNameMatchingEnabled,
  isNumericTargetId,
  migrateBaseNameToDefaultAccount,
  normalizeAccountId,
  sendPayloadWithChunkedTextAndMedia,
  setAccountEnabledInConfigSection,
} from "openclaw/plugin-sdk/zalouser";
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
import {
  listZalouserAccountIds,
  resolveDefaultZalouserAccountId,
  resolveZalouserAccountSync,
  getZcaUserInfo,
  checkZcaAuthenticated,
  type ResolvedZalouserAccount,
} from "./accounts.js";
import { ZalouserConfigSchema } from "./config-schema.js";
import { buildZalouserGroupCandidates, findZalouserGroupEntry } from "./group-policy.js";
import { resolveZalouserReactionMessageIds } from "./message-sid.js";
import { zalouserOnboardingAdapter } from "./onboarding.js";
import { probeZalouser } from "./probe.js";
import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
import { getZalouserRuntime } from "./runtime.js";
import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
import { collectZalouserStatusIssues } from "./status-issues.js";
import {
  listZaloFriendsMatching,
  listZaloGroupMembers,
  listZaloGroupsMatching,
  logoutZaloProfile,
  startZaloQrLogin,
  waitForZaloQrLogin,
  getZaloUserInfo,
} from "./zalo-js.js";

const meta = {
  id: "zalouser",
  label: "Zalo Personal",
  selectionLabel: "Zalo (Personal Account)",
  docsPath: "/channels/zalouser",
  docsLabel: "zalouser",
  blurb: "Zalo personal account via QR code login.",
  aliases: ["zlu"],
  order: 85,
  quickstartAllowFrom: true,
};

function stripZalouserTargetPrefix(raw: string): string {
  return raw
    .trim()
    .replace(/^(zalouser|zlu):/i, "")
    .trim();
}

function normalizePrefixedTarget(raw: string): string | undefined {
  const trimmed = stripZalouserTargetPrefix(raw);
  if (!trimmed) {
    return undefined;
  }

  const lower = trimmed.toLowerCase();
  if (lower.startsWith("group:")) {
    const id = trimmed.slice("group:".length).trim();
    return id ? `group:${id}` : undefined;
  }
  if (lower.startsWith("g:")) {
    const id = trimmed.slice("g:".length).trim();
    return id ? `group:${id}` : undefined;
  }
  if (lower.startsWith("user:")) {
    const id = trimmed.slice("user:".length).trim();
    return id ? `user:${id}` : undefined;
  }
  if (lower.startsWith("dm:")) {
    const id = trimmed.slice("dm:".length).trim();
    return id ? `user:${id}` : undefined;
  }
  if (lower.startsWith("u:")) {
    const id = trimmed.slice("u:".length).trim();
    return id ? `user:${id}` : undefined;
  }
  if (/^g-\S+$/i.test(trimmed)) {
    return `group:${trimmed}`;
  }
  if (/^u-\S+$/i.test(trimmed)) {
    return `user:${trimmed}`;
  }

  return trimmed;
}

function parseZalouserOutboundTarget(raw: string): {
  threadId: string;
  isGroup: boolean;
} {
  const normalized = normalizePrefixedTarget(raw);
  if (!normalized) {
    throw new Error("Zalouser target is required");
  }
  const lowered = normalized.toLowerCase();
  if (lowered.startsWith("group:")) {
    const threadId = normalized.slice("group:".length).trim();
    if (!threadId) {
      throw new Error("Zalouser group target is missing group id");
    }
    return { threadId, isGroup: true };
  }
  if (lowered.startsWith("user:")) {
    const threadId = normalized.slice("user:".length).trim();
    if (!threadId) {
      throw new Error("Zalouser user target is missing user id");
    }
    return { threadId, isGroup: false };
  }
  // Backward-compatible fallback for bare IDs.
  // Group sends should use explicit `group:<id>` targets.
  return { threadId: normalized, isGroup: false };
}

function parseZalouserDirectoryGroupId(raw: string): string {
  const normalized = normalizePrefixedTarget(raw);
  if (!normalized) {
    throw new Error("Zalouser group target is required");
  }
  const lowered = normalized.toLowerCase();
  if (lowered.startsWith("group:")) {
    const groupId = normalized.slice("group:".length).trim();
    if (!groupId) {
      throw new Error("Zalouser group target is missing group id");
    }
    return groupId;
  }
  if (lowered.startsWith("user:")) {
    throw new Error("Zalouser group members lookup requires a group target (group:<id>)");
  }
  return normalized;
}

function resolveZalouserQrProfile(accountId?: string | null): string {
  const normalized = normalizeAccountId(accountId);
  if (!normalized || normalized === DEFAULT_ACCOUNT_ID) {
    return process.env.ZALOUSER_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim() || "default";
  }
  return normalized;
}

function resolveZalouserOutboundChunkMode(cfg: OpenClawConfig, accountId?: string) {
  return getZalouserRuntime().channel.text.resolveChunkMode(cfg, "zalouser", accountId);
}

function resolveZalouserOutboundTextChunkLimit(cfg: OpenClawConfig, accountId?: string) {
  return getZalouserRuntime().channel.text.resolveTextChunkLimit(cfg, "zalouser", accountId, {
    fallbackLimit: zalouserDock.outbound?.textChunkLimit ?? 2000,
  });
}

function mapUser(params: {
  id: string;
  name?: string | null;
  avatarUrl?: string | null;
  raw?: unknown;
}): ChannelDirectoryEntry {
  return {
    kind: "user",
    id: params.id,
    name: params.name ?? undefined,
    avatarUrl: params.avatarUrl ?? undefined,
    raw: params.raw,
  };
}

function mapGroup(params: {
  id: string;
  name?: string | null;
  raw?: unknown;
}): ChannelDirectoryEntry {
  return {
    kind: "group",
    id: params.id,
    name: params.name ?? undefined,
    raw: params.raw,
  };
}

function resolveZalouserGroupPolicyEntry(params: ChannelGroupContext) {
  const account = resolveZalouserAccountSync({
    cfg: params.cfg,
    accountId: params.accountId ?? undefined,
  });
  const groups = account.config.groups ?? {};
  return findZalouserGroupEntry(
    groups,
    buildZalouserGroupCandidates({
      groupId: params.groupId,
      groupChannel: params.groupChannel,
      includeWildcard: true,
      allowNameMatching: isDangerousNameMatchingEnabled(account.config),
    }),
  );
}

function resolveZalouserGroupToolPolicy(
  params: ChannelGroupContext,
): GroupToolPolicyConfig | undefined {
  return resolveZalouserGroupPolicyEntry(params)?.tools;
}

function resolveZalouserRequireMention(params: ChannelGroupContext): boolean {
  const entry = resolveZalouserGroupPolicyEntry(params);
  if (typeof entry?.requireMention === "boolean") {
    return entry.requireMention;
  }
  return true;
}

const zalouserMessageActions: ChannelMessageActionAdapter = {
  listActions: ({ cfg }) => {
    const accounts = listZalouserAccountIds(cfg)
      .map((accountId) => resolveZalouserAccountSync({ cfg, accountId }))
      .filter((account) => account.enabled);
    if (accounts.length === 0) {
      return [];
    }
    return ["react"];
  },
  supportsAction: ({ action }) => action === "react",
  handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
    if (action !== "react") {
      throw new Error(`Zalouser action ${action} not supported`);
    }
    const account = resolveZalouserAccountSync({ cfg, accountId });
    const threadId =
      (typeof params.threadId === "string" ? params.threadId.trim() : "") ||
      (typeof params.to === "string" ? params.to.trim() : "") ||
      (typeof params.chatId === "string" ? params.chatId.trim() : "") ||
      (toolContext?.currentChannelId?.trim() ?? "");
    if (!threadId) {
      throw new Error("Zalouser react requires threadId (or to/chatId).");
    }
    const emoji = typeof params.emoji === "string" ? params.emoji.trim() : "";
    if (!emoji) {
      throw new Error("Zalouser react requires emoji.");
    }
    const ids = resolveZalouserReactionMessageIds({
      messageId: typeof params.messageId === "string" ? params.messageId : undefined,
      cliMsgId: typeof params.cliMsgId === "string" ? params.cliMsgId : undefined,
      currentMessageId: toolContext?.currentMessageId,
    });
    if (!ids) {
      throw new Error(
        "Zalouser react requires messageId + cliMsgId (or a current message context id).",
      );
    }
    const result = await sendReactionZalouser({
      profile: account.profile,
      threadId,
      isGroup: params.isGroup === true,
      msgId: ids.msgId,
      cliMsgId: ids.cliMsgId,
      emoji,
      remove: params.remove === true,
    });
    if (!result.ok) {
      throw new Error(result.error || "Failed to react on Zalo message");
    }
    return {
      content: [
        {
          type: "text" as const,
          text:
            params.remove === true
              ? `Removed reaction ${emoji} from ${ids.msgId}`
              : `Reacted ${emoji} on ${ids.msgId}`,
        },
      ],
      details: {
        messageId: ids.msgId,
        cliMsgId: ids.cliMsgId,
        threadId,
      },
    };
  },
};

export const zalouserDock: ChannelDock = {
  id: "zalouser",
  capabilities: {
    chatTypes: ["direct", "group"],
    media: true,
    blockStreaming: true,
  },
  outbound: { textChunkLimit: 2000 },
  config: {
    resolveAllowFrom: ({ cfg, accountId }) =>
      mapAllowFromEntries(resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom),
    formatAllowFrom: ({ allowFrom }) =>
      formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
  },
  groups: {
    resolveRequireMention: resolveZalouserRequireMention,
    resolveToolPolicy: resolveZalouserGroupToolPolicy,
  },
  threading: {
    resolveReplyToMode: () => "off",
  },
};

export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
  id: "zalouser",
  meta,
  onboarding: zalouserOnboardingAdapter,
  capabilities: {
    chatTypes: ["direct", "group"],
    media: true,
    reactions: true,
    threads: false,
    polls: false,
    nativeCommands: false,
    blockStreaming: true,
  },
  reload: { configPrefixes: ["channels.zalouser"] },
  configSchema: buildChannelConfigSchema(ZalouserConfigSchema),
  config: {
    listAccountIds: (cfg) => listZalouserAccountIds(cfg),
    resolveAccount: (cfg, accountId) => resolveZalouserAccountSync({ cfg: cfg, accountId }),
    defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg),
    setAccountEnabled: ({ cfg, accountId, enabled }) =>
      setAccountEnabledInConfigSection({
        cfg: cfg,
        sectionKey: "zalouser",
        accountId,
        enabled,
        allowTopLevel: true,
      }),
    deleteAccount: ({ cfg, accountId }) =>
      deleteAccountFromConfigSection({
        cfg: cfg,
        sectionKey: "zalouser",
        accountId,
        clearBaseFields: [
          "profile",
          "name",
          "dmPolicy",
          "allowFrom",
          "historyLimit",
          "groupAllowFrom",
          "groupPolicy",
          "groups",
          "messagePrefix",
        ],
      }),
    isConfigured: async (account) => await checkZcaAuthenticated(account.profile),
    describeAccount: (account): ChannelAccountSnapshot => ({
      accountId: account.accountId,
      name: account.name,
      enabled: account.enabled,
      configured: undefined,
    }),
    resolveAllowFrom: ({ cfg, accountId }) =>
      mapAllowFromEntries(resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom),
    formatAllowFrom: ({ allowFrom }) =>
      formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
  },
  security: {
    resolveDmPolicy: ({ cfg, accountId, account }) => {
      return buildAccountScopedDmSecurityPolicy({
        cfg,
        channelKey: "zalouser",
        accountId,
        fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
        policy: account.config.dmPolicy,
        allowFrom: account.config.allowFrom ?? [],
        policyPathSuffix: "dmPolicy",
        normalizeEntry: (raw) => raw.replace(/^(zalouser|zlu):/i, ""),
      });
    },
  },
  groups: {
    resolveRequireMention: resolveZalouserRequireMention,
    resolveToolPolicy: resolveZalouserGroupToolPolicy,
  },
  threading: {
    resolveReplyToMode: () => "off",
  },
  actions: zalouserMessageActions,
  setup: {
    resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
    applyAccountName: ({ cfg, accountId, name }) =>
      applyAccountNameToChannelSection({
        cfg: cfg,
        channelKey: "zalouser",
        accountId,
        name,
      }),
    validateInput: () => null,
    applyAccountConfig: ({ cfg, accountId, input }) => {
      const namedConfig = applyAccountNameToChannelSection({
        cfg: cfg,
        channelKey: "zalouser",
        accountId,
        name: input.name,
      });
      const next =
        accountId !== DEFAULT_ACCOUNT_ID
          ? migrateBaseNameToDefaultAccount({
              cfg: namedConfig,
              channelKey: "zalouser",
            })
          : namedConfig;
      return applySetupAccountConfigPatch({
        cfg: next,
        channelKey: "zalouser",
        accountId,
        patch: {},
      });
    },
  },
  messaging: {
    normalizeTarget: (raw) => normalizePrefixedTarget(raw),
    targetResolver: {
      looksLikeId: (raw) => {
        const normalized = normalizePrefixedTarget(raw);
        if (!normalized) {
          return false;
        }
        if (/^group:[^\s]+$/i.test(normalized) || /^user:[^\s]+$/i.test(normalized)) {
          return true;
        }
        return isNumericTargetId(normalized);
      },
      hint: "<user:id|group:id>",
    },
  },
  directory: {
    self: async ({ cfg, accountId }) => {
      const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
      const parsed = await getZaloUserInfo(account.profile);
      if (!parsed?.userId) {
        return null;
      }
      return mapUser({
        id: String(parsed.userId),
        name: parsed.displayName ?? null,
        avatarUrl: parsed.avatar ?? null,
        raw: parsed,
      });
    },
    listPeers: async ({ cfg, accountId, query, limit }) => {
      const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
      const friends = await listZaloFriendsMatching(account.profile, query);
      const rows = friends.map((friend) =>
        mapUser({
          id: String(friend.userId),
          name: friend.displayName ?? null,
          avatarUrl: friend.avatar ?? null,
          raw: friend,
        }),
      );
      return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
    },
    listGroups: async ({ cfg, accountId, query, limit }) => {
      const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
      const groups = await listZaloGroupsMatching(account.profile, query);
      const rows = groups.map((group) =>
        mapGroup({
          id: `group:${String(group.groupId)}`,
          name: group.name ?? null,
          raw: group,
        }),
      );
      return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
    },
    listGroupMembers: async ({ cfg, accountId, groupId, limit }) => {
      const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
      const normalizedGroupId = parseZalouserDirectoryGroupId(groupId);
      const members = await listZaloGroupMembers(account.profile, normalizedGroupId);
      const rows = members.map((member) =>
        mapUser({
          id: member.userId,
          name: member.displayName,
          avatarUrl: member.avatar ?? null,
          raw: member,
        }),
      );
      return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
    },
  },
  resolver: {
    resolveTargets: async ({ cfg, accountId, inputs, kind, runtime }) => {
      const results = [];
      for (const input of inputs) {
        const trimmed = input.trim();
        if (!trimmed) {
          results.push({ input, resolved: false, note: "empty input" });
          continue;
        }
        if (/^\d+$/.test(trimmed)) {
          results.push({ input, resolved: true, id: trimmed });
          continue;
        }
        try {
          const account = resolveZalouserAccountSync({
            cfg: cfg,
            accountId: accountId ?? DEFAULT_ACCOUNT_ID,
          });
          if (kind === "user") {
            const friends = await listZaloFriendsMatching(account.profile, trimmed);
            const best = friends[0];
            results.push({
              input,
              resolved: Boolean(best?.userId),
              id: best?.userId,
              name: best?.displayName,
              note: friends.length > 1 ? "multiple matches; chose first" : undefined,
            });
          } else {
            const groups = await listZaloGroupsMatching(account.profile, trimmed);
            const best =
              groups.find((group) => group.name.toLowerCase() === trimmed.toLowerCase()) ??
              groups[0];
            results.push({
              input,
              resolved: Boolean(best?.groupId),
              id: best?.groupId,
              name: best?.name,
              note: groups.length > 1 ? "multiple matches; chose first" : undefined,
            });
          }
        } catch (err) {
          runtime.error?.(`zalouser resolve failed: ${String(err)}`);
          results.push({ input, resolved: false, note: "lookup failed" });
        }
      }
      return results;
    },
  },
  pairing: {
    idLabel: "zalouserUserId",
    normalizeAllowEntry: (entry) => entry.replace(/^(zalouser|zlu):/i, ""),
    notifyApproval: async ({ cfg, id }) => {
      const account = resolveZalouserAccountSync({ cfg: cfg });
      const authenticated = await checkZcaAuthenticated(account.profile);
      if (!authenticated) {
        throw new Error("Zalouser not authenticated");
      }
      await sendMessageZalouser(id, "Your pairing request has been approved.", {
        profile: account.profile,
      });
    },
  },
  auth: {
    login: async ({ cfg, accountId, runtime }) => {
      const account = resolveZalouserAccountSync({
        cfg: cfg,
        accountId: accountId ?? DEFAULT_ACCOUNT_ID,
      });

      runtime.log(
        `Generating QR login for Zalo Personal (account: ${account.accountId}, profile: ${account.profile})...`,
      );

      const started = await startZaloQrLogin({
        profile: account.profile,
        timeoutMs: 35_000,
      });
      if (!started.qrDataUrl) {
        throw new Error(started.message || "Failed to start QR login");
      }

      const qrPath = await writeQrDataUrlToTempFile(started.qrDataUrl, account.profile);
      if (qrPath) {
        runtime.log(`Scan QR image: ${qrPath}`);
      } else {
        runtime.log("QR generated but could not be written to a temp file.");
      }

      const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 180_000 });
      if (!waited.connected) {
        throw new Error(waited.message || "Zalouser login failed");
      }

      runtime.log(waited.message);
    },
  },
  outbound: {
    deliveryMode: "direct",
    chunker: (text, limit) => getZalouserRuntime().channel.text.chunkMarkdownText(text, limit),
    chunkerMode: "markdown",
    sendPayload: async (ctx) =>
      await sendPayloadWithChunkedTextAndMedia({
        ctx,
        sendText: (nextCtx) => zalouserPlugin.outbound!.sendText!(nextCtx),
        sendMedia: (nextCtx) => zalouserPlugin.outbound!.sendMedia!(nextCtx),
        emptyResult: { channel: "zalouser", messageId: "" },
      }),
    sendText: async ({ to, text, accountId, cfg }) => {
      const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
      const target = parseZalouserOutboundTarget(to);
      const result = await sendMessageZalouser(target.threadId, text, {
        profile: account.profile,
        isGroup: target.isGroup,
        textMode: "markdown",
        textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
        textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId),
      });
      return buildChannelSendResult("zalouser", result);
    },
    sendMedia: async ({ to, text, mediaUrl, accountId, cfg, mediaLocalRoots }) => {
      const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
      const target = parseZalouserOutboundTarget(to);
      const result = await sendMessageZalouser(target.threadId, text, {
        profile: account.profile,
        isGroup: target.isGroup,
        mediaUrl,
        mediaLocalRoots,
        textMode: "markdown",
        textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
        textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId),
      });
      return buildChannelSendResult("zalouser", result);
    },
  },
  status: {
    defaultRuntime: {
      accountId: DEFAULT_ACCOUNT_ID,
      running: false,
      lastStartAt: null,
      lastStopAt: null,
      lastError: null,
    },
    collectStatusIssues: collectZalouserStatusIssues,
    buildChannelSummary: ({ snapshot }) => buildPassiveProbedChannelStatusSummary(snapshot),
    probeAccount: async ({ account, timeoutMs }) => probeZalouser(account.profile, timeoutMs),
    buildAccountSnapshot: async ({ account, runtime }) => {
      const configured = await checkZcaAuthenticated(account.profile);
      const configError = "not authenticated";
      const base = buildBaseAccountStatusSnapshot({
        account: {
          accountId: account.accountId,
          name: account.name,
          enabled: account.enabled,
          configured,
        },
        runtime: configured
          ? runtime
          : { ...runtime, lastError: runtime?.lastError ?? configError },
      });
      return {
        ...base,
        dmPolicy: account.config.dmPolicy ?? "pairing",
      };
    },
  },
  gateway: {
    startAccount: async (ctx) => {
      const account = ctx.account;
      let userLabel = "";
      try {
        const userInfo = await getZcaUserInfo(account.profile);
        if (userInfo?.displayName) {
          userLabel = ` (${userInfo.displayName})`;
        }
        ctx.setStatus({
          accountId: account.accountId,
          profile: userInfo,
        });
      } catch {
        // ignore probe errors
      }
      const statusSink = createAccountStatusSink({
        accountId: ctx.accountId,
        setStatus: ctx.setStatus,
      });
      ctx.log?.info(`[${account.accountId}] starting zalouser provider${userLabel}`);
      const { monitorZalouserProvider } = await import("./monitor.js");
      return monitorZalouserProvider({
        account,
        config: ctx.cfg,
        runtime: ctx.runtime,
        abortSignal: ctx.abortSignal,
        statusSink,
      });
    },
    loginWithQrStart: async (params) => {
      const profile = resolveZalouserQrProfile(params.accountId);
      return await startZaloQrLogin({
        profile,
        force: params.force,
        timeoutMs: params.timeoutMs,
      });
    },
    loginWithQrWait: async (params) => {
      const profile = resolveZalouserQrProfile(params.accountId);
      return await waitForZaloQrLogin({
        profile,
        timeoutMs: params.timeoutMs,
      });
    },
    logoutAccount: async (ctx) =>
      await logoutZaloProfile(ctx.account.profile || resolveZalouserQrProfile(ctx.accountId)),
  },
};

export type { ResolvedZalouserAccount };
