/**
 * OpenCode CLI 适配器(V1/V2 自动兼容门面)
 *
 * OpenCode 2.x 对 Server API 做了破坏性变更(端点迁移到 /api/ 前缀、要求认证、
 * 消息模型扁平化),本模块对外保持与原 V1 版 OpenCodeCLI 完全一致的接口,
 * 内部按以下策略自动选择适配器:
 *
 *   OpenCodeCLI(本文件,门面)
 *     ├── OpenCodeV1(v1.js,@opencode-ai/sdk)   → opencode 1.x / deveco-code
 *     └── OpenCodeV2(v2.js,@opencode/client)    → opencode 2.x
 *
 * 版本判定(detect.js):
 *   优先级:options.apiVersion > 环境变量 OPENCODE_API_VERSION > auto 自动探测
 *   auto 模式:
 *     - 显式配置了 baseUrl/host/port(或 OPENCODE_HOST/PORT)→ 仅探测该地址
 *       (/api/info 401 或 JSON ⇒ V2;/session JSON ⇒ V1、HTML ⇒ V2)
 *     - 未显式配置 → 先做 V2 后台服务发现(动态端口 + 注册文件自动认证,
 *       升级后"零配置"即用),失败再探测默认 localhost:4096
 *   V2 显式服务密码:options.password 或 OPENCODE_PASSWORD
 *   (`opencode serve` 启动时 stdout 打印 "server password ...")
 *
 * 自动切换:
 *   - 检测结果在实例内缓存,构造函数保持同步(与原版一致)
 *   - auto 模式下,调用失败且错误特征疑似"版本不匹配"(404/405/401、
 *     HTML 解析失败等)时自动失效缓存,下一次调用重新探测并切换适配器
 *     (典型场景:V1 服务原地升级为 V2 后长驻进程自动恢复)
 *   - 也可调用 redetect() 手动触发重探
 *
 * 依赖环境变量(可由 options 覆盖):
 *   OPENCODE_HOST        - 服务主机(默认 localhost)
 *   OPENCODE_PORT        - 服务端口(默认 4096;显式设置后将只探测该地址)
 *   OPENCODE_API_VERSION - 强制版本 'v1'|'v2'(默认 auto)
 *   OPENCODE_PASSWORD    - V2 显式服务密码
 *
 * @example
 *   import OpenCodeCLI from './opencode/index.js';
 *   const cli = new OpenCodeCLI({ cwd: '/workspace' });   // 同步构造,接口不变
 *   await cli.checkHealth();                               // 首次调用时完成版本探测
 *   cli.detectedVersion;                                   // → 'v1' | 'v2' | null
 */

import MessageArchive from '../../DbUse/MessageArchive.js';
import { createLogger } from '../../Routes/library/logger.js';
import { resolveOpenCodeTarget, normalizeVersion } from './detect.js';

class OpenCodeCLI {
    /** CLI 类型标识(写入存档 meta,供导出/审计区分底层 CLI)。子类通过 static 字段覆盖。 */
    static _cliType = 'opencode';

    /** 日志器名称。子类通过 static 字段覆盖。 */
    static _loggerName = 'OpenCodeCLI';

    /**
     * @param {object} options
     * @param {string} [options.host]       - 服务主机
     * @param {string} [options.port]       - 服务端口
     * @param {string} [options.baseUrl]    - 完整 baseUrl,优先级高于 host/port
     * @param {string} [options.apiVersion] - 'v1'|'v2'|'auto'(默认 auto)
     * @param {string} [options.password]   - V2 显式服务密码
     * @param {import('../../DbUse/MessageArchive.js').default} [options.archive] - 消息存档服务
     */
    constructor(options = {}) {
        this._options = options;
        // 版本策略:options > 环境变量 > auto
        this.apiVersion = normalizeVersion(options.apiVersion || process.env.OPENCODE_API_VERSION) || 'auto';
        // 预解析展示用 baseUrl(探测后可能被 V2 服务发现的实际地址覆盖)
        this.host = options.host || process.env.OPENCODE_HOST || 'localhost';
        this.port = options.port || process.env.OPENCODE_PORT || '4096';
        this.baseUrl = options.baseUrl || `http://${this.host}:${this.port}`;
        // CLI 类型标识 / 日志器名称(经 static 覆盖,子类构造期间即正确)
        this.cliType = this.constructor._cliType;
        this._loggerName = this.constructor._loggerName;
        // 消息存档:供归档/卡死检测使用,与内部适配器共享同一实例
        this.archive = options.archive ?? new MessageArchive();
        // 惰性初始化的内部适配器(OpenCodeV1 / OpenCodeV2)
        this._impl = null;
        this._detectPromise = null;
    }

    // ─── 版本探测与适配器管理 ──────────────────────────────────────────────────

    /** 已探测到的服务版本(未探测/探测失败时为 null) */
    get detectedVersion() {
        return this._impl?.apiVersion ?? null;
    }

    /** 当前配置的版本策略:'v1' | 'v2' | 'auto' */
    get versionPolicy() {
        return this.apiVersion;
    }

    /**
     * 手动触发重新探测(清除已缓存的适配器,下一次调用生效)。
     * 典型场景:目标服务刚完成版本升级/重启。
     */
    redetect() {
        this._impl = null;
        this._detectPromise = null;
    }

    /**
     * 惰性完成版本探测并构建适配器(并发安全,失败不缓存以便重试)
     * @returns {Promise<import('./base.js').default>}
     */
    async _ensureImpl() {
        if (this._impl) return this._impl;
        if (!this._detectPromise) {
            this._detectPromise = (async () => {
                const target = await resolveOpenCodeTarget(this._options);
                this._log('info',
                    `连接 OpenCode ${target.version.toUpperCase()} 服务 → ${target.baseUrl}` +
                    (target.detected ? '(自动探测)' : '(显式指定)'));
                // 动态加载对应适配器,避免同时加载两套 SDK
                const Impl = target.version === 'v2'
                    ? (await import('./v2.js')).default
                    : (await import('./v1.js')).default;
                this._impl = new Impl({
                    ...this._options,
                    baseUrl: target.baseUrl,
                    auth: target.auth ?? null,
                    password: target.auth?.password ?? this._options.password,
                    archive: this.archive
                });
                // 探测到的实际地址(V2 服务发现可能是动态端口)回写,供日志/上层展示
                this.baseUrl = target.baseUrl;
                return this._impl;
            })().catch(err => {
                // 探测失败不缓存,下一次调用重试(服务可能稍后才启动)
                this._detectPromise = null;
                throw err;
            });
        }
        return this._detectPromise;
    }

    /**
     * 委托调用内部适配器;auto 模式下失败特征疑似版本不匹配时失效缓存,
     * 下一次调用自动重新探测并切换适配器。
     * @template T
     * @param {(impl: import('./base.js').default) => Promise<T>} fn
     * @returns {Promise<T>}
     */
    async _withImpl(fn) {
        const impl = await this._ensureImpl();
        try {
            return await fn(impl);
        } catch (err) {
            if (this.apiVersion === 'auto' && this._looksLikeVersionMismatch(err)) {
                this._log('warn',
                    `调用失败且特征疑似版本不匹配(${err.message}),已失效版本缓存,下次调用将重新探测`);
                this.redetect();
            }
            throw err;
        }
    }

    /**
     * 判断错误是否疑似"适配器版本与服务版本不匹配"
     * 典型特征:V1 SDK 打到 V2 服务(404/405/401、HTML 解析失败、data.map 崩溃)
     */
    _looksLikeVersionMismatch(err) {
        if (err?.opencodeVersionMismatch) return true;
        const msg = String(err?.message || '');
        return /(\b40[145]\b)|not found|Unauthorized|Unexpected token|Unexpected '<'|\.map is not a function|reading 'map'|非 JSON/i.test(msg);
    }

    // ─── 运行状态检查 ──────────────────────────────────────────────────────────

    async checkHealth() {
        try {
            return await this._withImpl(impl => impl.checkHealth());
        } catch (error) {
            this._log('warn', `checkHealth: 连接失败 — ${error.message}`);
            return {
                status: 'error',
                message: `Health check failed: ${error.message}`,
                error: error.message
            };
        }
    }

    // ─── Session 管理(全部委托内部适配器) ────────────────────────────────────

    async listSessions() {
        return this._withImpl(impl => impl.listSessions());
    }

    async getSession(sessionId) {
        return this._withImpl(impl => impl.getSession(sessionId));
    }

    async deleteSession(sessionId) {
        return this._withImpl(impl => impl.deleteSession(sessionId));
    }

    async abortSession(sessionId) {
        return this._withImpl(impl => impl.abortSession(sessionId));
    }

    async sendMessage(sessionId, message) {
        return this._withImpl(impl => impl.sendMessage(sessionId, message));
    }

    /**
     * 向指定 Session 发送消息并同步等待 AI 回复完成
     * ⚠️ AI 回复可能耗时较长(几十秒到几分钟),调用方需设置合理的超时。
     * @returns {Promise<{ success: boolean, response: object, content: string }>}
     */
    async sendAndWaitMessage(sessionId, message) {
        return this._withImpl(impl => impl.sendAndWaitMessage(sessionId, message));
    }

    /**
     * 向指定 Session 注入提示词/上下文(不触发 AI 响应)
     * V1: noReply:true;V2: synthetic 消息
     */
    async injectPrompt(sessionId, message) {
        return this._withImpl(impl => impl.injectPrompt(sessionId, message));
    }

    /**
     * 创建新 Session,可选附带初始提示词
     * @param {string} title  - Session 标题
     * @param {string} [prompt] - 初始提示词(可选)
     * @param {object} [options]
     * @param {string} [options.cwd] - 工作目录
     */
    async startSession(title, prompt, options = {}) {
        return this._withImpl(impl => impl.startSession(title, prompt, options));
    }

    async checkSessionStatus(sessionId) {
        return this._withImpl(impl => impl.checkSessionStatus(sessionId));
    }

    async getSessionMessages(sessionId) {
        return this._withImpl(impl => impl.getSessionMessages(sessionId));
    }

    async getLiveMessages(sessionId) {
        return this._withImpl(impl => impl.getLiveMessages(sessionId));
    }

    async exportSession(sessionId, opts = {}) {
        return this._withImpl(impl => impl.exportSession(sessionId, opts));
    }

    // ─── 本地存档直读(无需连接服务) ──────────────────────────────────────────

    /** 读取本地已归档的消息(不请求远端,用于事后审计/对比) */
    async getArchivedMessages(sessionId) {
        return this.archive.readMessages(sessionId);
    }

    /** 读取本地存档元信息(含 lastResponseAt / archivedCount 等) */
    async getArchiveMeta(sessionId) {
        return this.archive.readMeta(sessionId);
    }

    // ─── 内部方法 ──────────────────────────────────────────────────────────────

    _log(level, msg) {
        const logger = createLogger(this._loggerName);
        switch (level) {
            case 'error':
                logger.error(`❌ ${msg}`);
                break;
            case 'warn':
                logger.warn(`⚠️  ${msg}`);
                break;
            case 'debug':
                logger.debug(`🔍 ${msg}`);
                break;
            default:
                logger.info(`ℹ️  ${msg}`);
        }
    }
}

/**
 * 异步工厂:创建门面实例并立即完成版本探测(失败抛出)。
 * 适合需要立刻确认连接目标的场景;普通场景直接 new OpenCodeCLI() 即可。
 *
 * @param {object} [options] - 同 OpenCodeCLI 构造参数
 * @returns {Promise<OpenCodeCLI>}
 */
export async function createOpenCodeCLI(options = {}) {
    const cli = new OpenCodeCLI(options);
    await cli._ensureImpl();
    return cli;
}

export default OpenCodeCLI;