import type { ITelegramClient, TelegramClientError } from './telegramClient';
import type { TelegramUpdate } from './types';
import { logError, logInfo } from '../../utils';

/**
 * TelegramBot — long-polling loop with explicit lifecycle.
 *
 * Why this is split from the HTTP client:
 *   - The HTTP client knows how to make a single request; the bot knows how to
 *     coordinate a polling loop, an offset cursor, error backoff, and a clean
 *     shutdown via AbortController.
 *   - This separation makes the bot mock-friendly — tests inject a stub client
 *     that returns scripted update arrays without touching the network.
 *
 * Behavior:
 *   - On `start()`: kicks off an async loop that calls `client.getUpdates()`
 *     with a 25s timeout. The loop catches per-iteration errors so a single
 *     network blip cannot tear down the bot.
 *   - On `stop()`: aborts any in-flight request and exits the loop. Idempotent.
 *   - On `aborted` errors during a normal shutdown: silently exits.
 *   - On `no-token`/`api(401)` (token revoked / wrong): logs once, stops.
 *   - On generic `network` errors: exponential backoff capped at 30s.
 *
 * The handler signature is `(text, chatId) => Promise<string | null>`. Returning
 * null suppresses the reply (e.g. for ignored messages).
 */

export type TelegramMessageHandler = (text: string, chatId: number) => Promise<string | null>;

export interface TelegramBotDeps {
    client: ITelegramClient;
    handle: TelegramMessageHandler;
    /** Long-poll seconds passed to getUpdates. Default 25. */
    pollTimeoutSec?: number;
    /** Per-call wait when an error happens. Capped at maxBackoffMs. Default 1000ms. */
    initialBackoffMs?: number;
    /** Default 30000ms. */
    maxBackoffMs?: number;
    /** Optional sleep override for tests (defaults to setTimeout-based). */
    sleep?: (ms: number) => Promise<void>;
}

const defaultSleep = (ms: number) =>
    new Promise<void>((resolve) => {
        const t = setTimeout(resolve, ms);
        // Avoid blocking node exit if the bot is the only thing keeping the loop alive.
        if (typeof t === 'object' && t && 'unref' in t) (t as any).unref();
    });

export interface EnrolledChat {
    chatId: number;
    username?: string;
    firstName?: string;
}

export class TelegramBot {
    private _running = false;
    private _abort: AbortController | undefined;
    private _offset: number | undefined;
    private _loopPromise: Promise<void> | undefined;
    private _enrollPending: {
        resolve: (chat: EnrolledChat) => void;
        reject: (err: Error) => void;
    } | undefined;

    constructor(private readonly _deps: TelegramBotDeps) {}

    isRunning(): boolean { return this._running; }

    /**
     * Wait for the next incoming message and resolve with its chat info.
     *
     * Used by the settings wizard: the user clicks "내 chat ID 자동 등록", we
     * arm this one-shot capture, and the very next incoming Telegram message
     * is intercepted (no AI reply, no handler call) and yielded back as the
     * captured chat. The bot keeps polling normally afterwards.
     *
     * Only one enrollment can be pending at a time — calling this while
     * already armed rejects the prior pending promise.
     */
    enrollNextChat(timeoutMs: number = 120000): Promise<EnrolledChat> {
        if (this._enrollPending) {
            this._enrollPending.reject(new Error('Superseded by a new enrollment request.'));
            this._enrollPending = undefined;
        }
        return new Promise<EnrolledChat>((resolve, reject) => {
            const timer = setTimeout(() => {
                if (this._enrollPending && this._enrollPending.resolve === wrappedResolve) {
                    this._enrollPending = undefined;
                    reject(new Error(`No message received within ${Math.round(timeoutMs / 1000)}s.`));
                }
            }, timeoutMs);
            if (typeof timer === 'object' && timer && 'unref' in timer) (timer as any).unref();

            const wrappedResolve = (chat: EnrolledChat) => {
                clearTimeout(timer);
                resolve(chat);
            };
            const wrappedReject = (err: Error) => {
                clearTimeout(timer);
                reject(err);
            };
            this._enrollPending = { resolve: wrappedResolve, reject: wrappedReject };
        });
    }

    /** Cancel any pending enrollment without resolving it. */
    cancelEnrollment(): void {
        if (!this._enrollPending) return;
        this._enrollPending.reject(new Error('Enrollment cancelled.'));
        this._enrollPending = undefined;
    }

    /** Idempotent: starts the long-poll loop if not already running. */
    start(): void {
        if (this._running) return;
        this._running = true;
        this._abort = new AbortController();
        this._loopPromise = this._loop().catch((e) => {
            logError('Telegram bot loop crashed unexpectedly.', { error: e?.message ?? String(e) });
        });
        logInfo('Telegram bot started.');
    }

    /** Idempotent: aborts the in-flight call and waits for the loop to exit. */
    async stop(): Promise<void> {
        if (!this._running) return;
        this._running = false;
        try { this._abort?.abort(); } catch { /* noop */ }
        const p = this._loopPromise;
        this._abort = undefined;
        this._loopPromise = undefined;
        if (this._enrollPending) {
            this._enrollPending.reject(new Error('Bot stopped before enrollment completed.'));
            this._enrollPending = undefined;
        }
        if (p) {
            try { await p; } catch { /* swallow — already logged */ }
        }
        logInfo('Telegram bot stopped.');
    }

    private async _loop(): Promise<void> {
        const { client, handle } = this._deps;
        const pollTimeoutSec = this._deps.pollTimeoutSec ?? 25;
        const initialBackoff = this._deps.initialBackoffMs ?? 1000;
        const maxBackoff = this._deps.maxBackoffMs ?? 30000;
        const sleep = this._deps.sleep ?? defaultSleep;

        let backoff = initialBackoff;

        while (this._running) {
            const signal = this._abort?.signal;
            try {
                const updates = await client.getUpdates({
                    offset: this._offset,
                    timeoutSec: pollTimeoutSec,
                    signal,
                });
                backoff = initialBackoff; // reset on success

                for (const update of updates) {
                    if (!this._running) break;
                    this._offset = update.update_id + 1;
                    await this._processUpdate(update, handle);
                }
            } catch (e: any) {
                if (!this._running) break;
                const err = e as TelegramClientError;
                if (err?.kind === 'aborted') break;
                if (err?.kind === 'no-token') {
                    logError('Telegram bot stopping: token not configured.');
                    this._running = false;
                    break;
                }
                if (err?.kind === 'api' && (err.statusCode === 401 || err.statusCode === 404)) {
                    logError('Telegram bot stopping: invalid token (HTTP 401/404).', { statusCode: err.statusCode });
                    this._running = false;
                    break;
                }
                // Generic network / api errors: log and back off.
                logError('Telegram poll error; backing off.', { backoff, error: e?.message ?? String(e) });
                await sleep(backoff);
                backoff = Math.min(backoff * 2, maxBackoff);
            }
        }
    }

    private async _processUpdate(update: TelegramUpdate, handle: TelegramMessageHandler): Promise<void> {
        const msg = update.message ?? update.edited_message;
        if (!msg) return;
        const text = msg.text?.trim();
        const chatId = msg.chat?.id;
        if (!text || typeof chatId !== 'number') return;

        // Enrollment intercept: if the settings wizard armed enrollNextChat(),
        // hand this update off and skip the normal AI handler. We still send a
        // friendly acknowledgement so the user knows enrollment worked.
        if (this._enrollPending) {
            const pending = this._enrollPending;
            this._enrollPending = undefined;
            pending.resolve({
                chatId,
                username: msg.from?.username,
                firstName: msg.from?.first_name,
            });
            try {
                await this._deps.client.sendMessage({
                    chatId,
                    text: '✅ 채널이 등록되었습니다. 이제부터 메시지를 보낼 수 있어요.',
                    signal: this._abort?.signal,
                });
            } catch (e: any) {
                logError('Telegram enrollment ack send failed.', { chatId, error: e?.message ?? String(e) });
            }
            return;
        }

        let reply: string | null = null;
        try {
            reply = await handle(text, chatId);
        } catch (e: any) {
            logError('Telegram message handler threw.', { chatId, error: e?.message ?? String(e) });
            reply = `⚠️ Astra 처리 중 오류: ${e?.message ?? e}`;
        }

        if (reply == null) {
            // Handler intentionally suppressed (e.g. allowlist drop). Different
            // from an empty string — empty means we tried and got nothing back,
            // which is a bug we want to know about.
            return;
        }
        if (!reply.trim()) {
            logError('Telegram reply was empty after handle(). Sending placeholder so the user knows the bot is alive.', { chatId });
            reply = '⚠️ 빈 응답이 생성되었습니다. 다시 시도해주세요.';
        }

        // One-shot retry on transient send failures (network blip, Telegram
        // rate-limit). The previous behavior — log once and move on — was the
        // most likely cause of the "messages disappear sometimes" report:
        // the AI replied successfully, but the send never reached Telegram
        // and we never retried.
        let sent = false;
        for (let attempt = 0; attempt < 2 && !sent; attempt++) {
            try {
                await this._deps.client.sendMessage({
                    chatId,
                    text: reply,
                    signal: this._abort?.signal,
                });
                sent = true;
                logInfo('Telegram reply sent.', { chatId, chars: reply.length, attempt });
            } catch (e: any) {
                logError('Telegram reply send failed.', {
                    chatId, attempt, error: e?.message ?? String(e),
                });
                if (attempt === 0 && this._running) {
                    // Brief backoff before the retry — don't tight-loop on a
                    // 429 rate limit.
                    await new Promise((r) => {
                        const t = setTimeout(r, 1500);
                        if (typeof t === 'object' && t && 'unref' in t) (t as any).unref();
                    });
                } else {
                    break;
                }
            }
        }
    }
}
