/**
 * Web Fetch — Bridge 무관 직접 URL fetch (vscode 의존 없음 — 테스트 용이).
 *
 * 배경: 일반 챗의 URL 주입(urlContext.ts)은 Datacollect Bridge(:3002)에 100%
 * 의존했다. Bridge가 꺼져 있으면 — 확장은 Bridge를 자동 시작하지 않는다 —
 * "접근 실패" 블록이 떠서 모델이 "사이트 방문 불가"라고 답하는 공백이 있었다.
 * 이 모듈은 그 폴백: extension host의 global fetch(Node 18+, bridgeClient가
 * 이미 사용 중)로 직접 페이지를 가져와 본문 텍스트를 추출한다.
 *
 * 설계 원칙: 절대 throw 하지 않는다 — 모든 실패는 {ok:false, error} 로 반환.
 */

/**
 * 스킴 없는 도메인 인식용 보수적 TLD 목록 — 사용자는 "koritips.com 가서 분석해줘"
 * 처럼 https:// 를 생략하기 마련이라, 흔한 TLD 만 허용해 파일명(utils.ts,
 * package.json 등) 오인을 차단한다.
 */
const BARE_DOMAIN_TLDS = 'com|net|org|io|co|kr|jp|dev|app|ai|me|info|blog|shop|site|xyz|cc|tv|us|uk|edu|gov';

/**
 * http(s) URL 추출 — dedupe + trailing 구두점 제거. 슬래시 명령은 자체 처리하므로
 * 제외. https:// 가 없는 bare 도메인(koritips.com, www.foo.net 등)도 인식해
 * https:// 를 붙여 반환한다.
 */
export function extractUrls(text: string, max = 2): string[] {
    const t = (text || '').trim();
    // 슬래시 *명령* (/wikify 등)만 제외 — 절대경로("/Volumes/... koritips.com 봐줘")로
    // 시작하는 프롬프트는 URL 추출 대상이다 (startsWith('/') 는 경로를 오인했던 버그).
    if (!t || /^\/[a-zA-Z][\w-]*(\s|$)/.test(t)) return [];
    const seen = new Set<string>();
    const out: string[] = [];

    // ① 스킴 있는 URL 우선.
    const re = /https?:\/\/[^\s<>"'`)\]]+/gi;
    let m: RegExpExecArray | null;
    while ((m = re.exec(t)) !== null && out.length < max) {
        // 문장 끝 구두점이 URL 에 붙는 흔한 오염 제거.
        const url = m[0].replace(/[.,;:!?…」』)]+$/, '');
        if (!seen.has(url)) {
            seen.add(url);
            out.push(url);
        }
    }
    if (out.length >= max) return out;

    // ② Bare 도메인 — 이미 찾은 URL 영역은 마스킹해 이중 매칭 방지.
    //    직전 문자가 @(이메일)·/(경로 일부)·.(서브파트) 면 제외.
    let masked = t;
    for (const u of out) masked = masked.split(u).join(' '.repeat(Math.min(u.length, 8)));
    const bareRe = new RegExp(
        `(^|[\\s("'\`「『<>])((?:[a-z0-9-]+\\.)+(?:${BARE_DOMAIN_TLDS})(?:\\.[a-z]{2})?(?::\\d{2,5})?(?:/[^\\s<>"'\`)\\]]*)?)`,
        'gi',
    );
    while ((m = bareRe.exec(masked)) !== null && out.length < max) {
        const candidate = m[2].replace(/[.,;:!?…」』)]+$/, '');
        if (!candidate.includes('.')) continue;
        const url = `https://${candidate}`;
        if (!seen.has(url) && !seen.has(`http://${candidate}`)) {
            seen.add(url);
            out.push(url);
        }
    }
    return out;
}

export interface WebFetchResult {
    ok: boolean;
    url: string;
    title: string;
    text: string;
    error?: string;
}

const DEFAULT_TIMEOUT_MS = 15_000;
const DEFAULT_MAX_CHARS = 20_000;

/**
 * URL 본문을 직접 fetch 해 텍스트로 변환. HTML 이면 태그를 걷어내고,
 * 그 외(text/json 등)는 원문 그대로 cap. http/https 만 허용.
 */
export async function fetchUrlDirect(
    url: string,
    opts: { timeoutMs?: number; maxChars?: number } = {},
): Promise<WebFetchResult> {
    const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
    const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
    const fail = (error: string): WebFetchResult => ({ ok: false, url, title: '', text: '', error });

    if (!/^https?:\/\//i.test(url)) return fail('http/https URL만 지원합니다.');
    if (typeof fetch !== 'function') return fail('이 환경은 직접 fetch를 지원하지 않습니다.');

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const res = await fetch(url, {
            signal: controller.signal,
            redirect: 'follow',
            headers: {
                // 일부 사이트가 UA 없는 요청을 차단 — 평범한 브라우저 UA 로.
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
                'Accept': 'text/html,application/xhtml+xml,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5',
            },
        });
        if (!res.ok) return fail(`HTTP ${res.status} ${res.statusText || ''}`.trim());
        const contentType = (res.headers.get('content-type') || '').toLowerCase();
        const raw = await res.text();
        if (!raw.trim()) return fail('응답 본문이 비어 있습니다.');

        if (contentType.includes('html') || /^\s*<(!doctype|html)/i.test(raw)) {
            const title = _extractTitle(raw);
            const text = htmlToText(raw).slice(0, maxChars);
            if (text.trim().length < 50) {
                return fail('본문 추출 실패 (콘텐츠 없음 또는 JS 전용 렌더링).');
            }
            return { ok: true, url, title, text };
        }
        return { ok: true, url, title: '', text: raw.slice(0, maxChars) };
    } catch (e: any) {
        const msg = e?.name === 'AbortError'
            ? `타임아웃 (${Math.round(timeoutMs / 1000)}s)`
            : String(e?.message ?? e).slice(0, 120);
        return fail(msg);
    } finally {
        clearTimeout(timer);
    }
}

function _extractTitle(html: string): string {
    const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
    return m ? decodeEntities(m[1]).replace(/\s+/g, ' ').trim().slice(0, 200) : '';
}

/** HTML → 평문. script/style/noscript 제거 → 블록 태그를 줄바꿈으로 → 태그 strip → 엔티티 → 공백 정리. */
export function htmlToText(html: string): string {
    let s = html
        .replace(/<script[\s\S]*?<\/script>/gi, ' ')
        .replace(/<style[\s\S]*?<\/style>/gi, ' ')
        .replace(/<noscript[\s\S]*?<\/noscript>/gi, ' ')
        .replace(/<!--[\s\S]*?-->/g, ' ');
    // 블록 요소 경계를 줄바꿈으로 보존 — 문단 구조가 텍스트에도 남게.
    s = s.replace(/<\/(p|div|section|article|li|tr|h[1-6]|blockquote|pre)>/gi, '\n')
        .replace(/<(br|hr)\s*\/?>/gi, '\n');
    s = s.replace(/<[^>]+>/g, ' ');
    s = decodeEntities(s);
    // 공백 정리: 줄 내 다중 공백 → 1개, 3개 이상 연속 줄바꿈 → 2개.
    return s
        .split('\n')
        .map((line) => line.replace(/\s+/g, ' ').trim())
        .join('\n')
        .replace(/\n{3,}/g, '\n\n')
        .trim();
}

/** 최소 엔티티 디코드 — 본문 가독에 필요한 흔한 것만. */
export function decodeEntities(s: string): string {
    return s
        .replace(/&nbsp;/gi, ' ')
        .replace(/&amp;/gi, '&')
        .replace(/&lt;/gi, '<')
        .replace(/&gt;/gi, '>')
        .replace(/&quot;/gi, '"')
        .replace(/&#0?39;|&apos;/gi, "'")
        .replace(/&#(\d+);/g, (_, code) => {
            const n = Number(code);
            return n > 0 && n < 0x110000 ? String.fromCodePoint(n) : '';
        });
}
