import { getChannelPlugin } from "../../channels/plugins/index.js";
import { defaultRuntime } from "../../runtime.js";
import { buildDirectoryCacheKey, DirectoryCache } from "./directory-cache.js";
import { buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider, } from "./target-normalization.js";
import { ambiguousTargetError, unknownTargetError } from "./target-errors.js";
export async function resolveChannelTarget(params) {
    return resolveMessagingTarget(params);
}
const CACHE_TTL_MS = 30 * 60 * 1000;
const directoryCache = new DirectoryCache(CACHE_TTL_MS);
export function resetDirectoryCache(params) {
    if (!params?.channel) {
        directoryCache.clear();
        return;
    }
    const channelKey = params.channel;
    const accountKey = params.accountId ?? "default";
    directoryCache.clearMatching((key) => {
        if (!key.startsWith(`${channelKey}:`))
            return false;
        if (!params.accountId)
            return true;
        return key.startsWith(`${channelKey}:${accountKey}:`);
    });
}
function normalizeQuery(value) {
    return value.trim().toLowerCase();
}
function stripTargetPrefixes(value) {
    return value
        .replace(/^(channel|user):/i, "")
        .replace(/^[@#]/, "")
        .trim();
}
export function formatTargetDisplay(params) {
    const plugin = getChannelPlugin(params.channel);
    if (plugin?.messaging?.formatTargetDisplay) {
        return plugin.messaging.formatTargetDisplay({
            target: params.target,
            display: params.display,
            kind: params.kind,
        });
    }
    const trimmedTarget = params.target.trim();
    const lowered = trimmedTarget.toLowerCase();
    const display = params.display?.trim();
    const kind = params.kind ??
        (lowered.startsWith("user:") ? "user" : lowered.startsWith("channel:") ? "group" : undefined);
    if (display) {
        if (display.startsWith("#") || display.startsWith("@"))
            return display;
        if (kind === "user")
            return `@${display}`;
        if (kind === "group" || kind === "channel")
            return `#${display}`;
        return display;
    }
    if (!trimmedTarget)
        return trimmedTarget;
    if (trimmedTarget.startsWith("#") || trimmedTarget.startsWith("@"))
        return trimmedTarget;
    const channelPrefix = `${params.channel}:`;
    const withoutProvider = trimmedTarget.toLowerCase().startsWith(channelPrefix)
        ? trimmedTarget.slice(channelPrefix.length)
        : trimmedTarget;
    const withoutPrefix = withoutProvider.replace(/^telegram:/i, "");
    if (/^channel:/i.test(withoutPrefix)) {
        return `#${withoutPrefix.replace(/^channel:/i, "")}`;
    }
    if (/^user:/i.test(withoutPrefix)) {
        return `@${withoutPrefix.replace(/^user:/i, "")}`;
    }
    return withoutPrefix;
}
function preserveTargetCase(channel, raw, normalized) {
    if (channel !== "slack")
        return normalized;
    const trimmed = raw.trim();
    if (/^channel:/i.test(trimmed) || /^user:/i.test(trimmed))
        return trimmed;
    if (trimmed.startsWith("#"))
        return `channel:${trimmed.slice(1).trim()}`;
    if (trimmed.startsWith("@"))
        return `user:${trimmed.slice(1).trim()}`;
    return trimmed;
}
function detectTargetKind(channel, raw, preferred) {
    if (preferred)
        return preferred;
    const trimmed = raw.trim();
    if (!trimmed)
        return "group";
    if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed))
        return "user";
    if (trimmed.startsWith("#") || /^channel:/i.test(trimmed))
        return "group";
    // For some channels (e.g., BlueBubbles/iMessage), bare phone numbers are almost always DM targets.
    if ((channel === "bluebubbles" || channel === "imessage") && /^\+?\d{6,}$/.test(trimmed)) {
        return "user";
    }
    return "group";
}
function normalizeDirectoryEntryId(channel, entry) {
    const normalized = normalizeTargetForProvider(channel, entry.id);
    return normalized ?? entry.id.trim();
}
function matchesDirectoryEntry(params) {
    const query = normalizeQuery(params.query);
    if (!query)
        return false;
    const id = stripTargetPrefixes(normalizeDirectoryEntryId(params.channel, params.entry));
    const name = params.entry.name ? stripTargetPrefixes(params.entry.name) : "";
    const handle = params.entry.handle ? stripTargetPrefixes(params.entry.handle) : "";
    const candidates = [id, name, handle].map((value) => normalizeQuery(value)).filter(Boolean);
    return candidates.some((value) => value === query || value.includes(query));
}
function resolveMatch(params) {
    const matches = params.entries.filter((entry) => matchesDirectoryEntry({ channel: params.channel, entry, query: params.query }));
    if (matches.length === 0)
        return { kind: "none" };
    if (matches.length === 1)
        return { kind: "single", entry: matches[0] };
    return { kind: "ambiguous", entries: matches };
}
async function listDirectoryEntries(params) {
    const plugin = getChannelPlugin(params.channel);
    const directory = plugin?.directory;
    if (!directory)
        return [];
    const runtime = params.runtime ?? defaultRuntime;
    const useLive = params.source === "live";
    if (params.kind === "user") {
        const fn = useLive ? (directory.listPeersLive ?? directory.listPeers) : directory.listPeers;
        if (!fn)
            return [];
        return await fn({
            cfg: params.cfg,
            accountId: params.accountId ?? undefined,
            query: params.query ?? undefined,
            limit: undefined,
            runtime,
        });
    }
    const fn = useLive ? (directory.listGroupsLive ?? directory.listGroups) : directory.listGroups;
    if (!fn)
        return [];
    return await fn({
        cfg: params.cfg,
        accountId: params.accountId ?? undefined,
        query: params.query ?? undefined,
        limit: undefined,
        runtime,
    });
}
async function getDirectoryEntries(params) {
    const signature = buildTargetResolverSignature(params.channel);
    const cacheKey = buildDirectoryCacheKey({
        channel: params.channel,
        accountId: params.accountId,
        kind: params.kind,
        source: "cache",
        signature,
    });
    const cached = directoryCache.get(cacheKey, params.cfg);
    if (cached)
        return cached;
    const entries = await listDirectoryEntries({
        cfg: params.cfg,
        channel: params.channel,
        accountId: params.accountId,
        kind: params.kind,
        query: params.query,
        runtime: params.runtime,
        source: "cache",
    });
    if (entries.length > 0 || !params.preferLiveOnMiss) {
        directoryCache.set(cacheKey, entries, params.cfg);
        return entries;
    }
    const liveKey = buildDirectoryCacheKey({
        channel: params.channel,
        accountId: params.accountId,
        kind: params.kind,
        source: "live",
        signature,
    });
    const liveEntries = await listDirectoryEntries({
        cfg: params.cfg,
        channel: params.channel,
        accountId: params.accountId,
        kind: params.kind,
        query: params.query,
        runtime: params.runtime,
        source: "live",
    });
    directoryCache.set(liveKey, liveEntries, params.cfg);
    directoryCache.set(cacheKey, liveEntries, params.cfg);
    return liveEntries;
}
function pickAmbiguousMatch(entries, mode) {
    if (entries.length === 0)
        return null;
    if (mode === "first")
        return entries[0] ?? null;
    const ranked = entries.map((entry) => ({
        entry,
        rank: typeof entry.rank === "number" ? entry.rank : 0,
    }));
    const bestRank = Math.max(...ranked.map((item) => item.rank));
    const best = ranked.find((item) => item.rank === bestRank)?.entry;
    return best ?? entries[0] ?? null;
}
export async function resolveMessagingTarget(params) {
    const raw = normalizeChannelTargetInput(params.input);
    if (!raw) {
        return { ok: false, error: new Error("Target is required") };
    }
    const plugin = getChannelPlugin(params.channel);
    const providerLabel = plugin?.meta?.label ?? params.channel;
    const hint = plugin?.messaging?.targetResolver?.hint;
    const kind = detectTargetKind(params.channel, raw, params.preferredKind);
    const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw;
    const looksLikeTargetId = () => {
        const trimmed = raw.trim();
        if (!trimmed)
            return false;
        const lookup = plugin?.messaging?.targetResolver?.looksLikeId;
        if (lookup)
            return lookup(trimmed, normalized);
        if (/^(channel|group|user):/i.test(trimmed))
            return true;
        if (/^[@#]/.test(trimmed))
            return true;
        if (/^\+?\d{6,}$/.test(trimmed)) {
            // BlueBubbles/iMessage phone numbers should usually resolve via the directory to a DM chat,
            // otherwise the provider may pick an existing group containing that handle.
            if (params.channel === "bluebubbles" || params.channel === "imessage")
                return false;
            return true;
        }
        if (trimmed.includes("@thread"))
            return true;
        if (/^(conversation|user):/i.test(trimmed))
            return true;
        // Feishu ID formats: oc_ (chat), ou_ (user open_id), on_ (union_id)
        if (params.channel === "feishu" && /^o[cun]_[a-zA-Z0-9]+$/.test(trimmed))
            return true;
        return false;
    };
    if (looksLikeTargetId()) {
        const directTarget = preserveTargetCase(params.channel, raw, normalized);
        return {
            ok: true,
            target: {
                to: directTarget,
                kind,
                display: stripTargetPrefixes(raw),
                source: "normalized",
            },
        };
    }
    const query = stripTargetPrefixes(raw);
    const entries = await getDirectoryEntries({
        cfg: params.cfg,
        channel: params.channel,
        accountId: params.accountId,
        kind: kind === "user" ? "user" : "group",
        query,
        runtime: params.runtime,
        preferLiveOnMiss: true,
    });
    const match = resolveMatch({ channel: params.channel, entries, query });
    if (match.kind === "single") {
        const entry = match.entry;
        return {
            ok: true,
            target: {
                to: normalizeDirectoryEntryId(params.channel, entry),
                kind,
                display: entry.name ?? entry.handle ?? stripTargetPrefixes(entry.id),
                source: "directory",
            },
        };
    }
    if (match.kind === "ambiguous") {
        const mode = params.resolveAmbiguous ?? "error";
        if (mode !== "error") {
            const best = pickAmbiguousMatch(match.entries, mode);
            if (best) {
                return {
                    ok: true,
                    target: {
                        to: normalizeDirectoryEntryId(params.channel, best),
                        kind,
                        display: best.name ?? best.handle ?? stripTargetPrefixes(best.id),
                        source: "directory",
                    },
                };
            }
        }
        return {
            ok: false,
            error: ambiguousTargetError(providerLabel, raw, hint),
            candidates: match.entries,
        };
    }
    // For iMessage-style channels, allow sending directly to the normalized handle
    // even if the directory doesn't contain an entry yet.
    if ((params.channel === "bluebubbles" || params.channel === "imessage") &&
        /^\+?\d{6,}$/.test(query)) {
        const directTarget = preserveTargetCase(params.channel, raw, normalized);
        return {
            ok: true,
            target: {
                to: directTarget,
                kind,
                display: stripTargetPrefixes(raw),
                source: "normalized",
            },
        };
    }
    return {
        ok: false,
        error: unknownTargetError(providerLabel, raw, hint),
    };
}
export async function lookupDirectoryDisplay(params) {
    const normalized = normalizeTargetForProvider(params.channel, params.targetId) ?? params.targetId;
    // Targets can resolve to either peers (DMs) or groups. Try both.
    const [groups, users] = await Promise.all([
        getDirectoryEntries({
            cfg: params.cfg,
            channel: params.channel,
            accountId: params.accountId,
            kind: "group",
            runtime: params.runtime,
            preferLiveOnMiss: false,
        }),
        getDirectoryEntries({
            cfg: params.cfg,
            channel: params.channel,
            accountId: params.accountId,
            kind: "user",
            runtime: params.runtime,
            preferLiveOnMiss: false,
        }),
    ]);
    const findMatch = (candidates) => candidates.find((candidate) => normalizeDirectoryEntryId(params.channel, candidate) === normalized);
    const entry = findMatch(groups) ?? findMatch(users);
    return entry?.name ?? entry?.handle ?? undefined;
}
