// ==UserScript==
// @name         ntk01 Downloader
// @namespace    https://github.com/crossSiteKikyo/tokiDownloader
// @version      0.0.1
// @description  ntk01.com 웹툰/만화 다운로더
// @author       hehaho
// @match        https://ntk01.com/*
// @match        https://www.ntk01.com/*
// @icon         https://github.com/user-attachments/assets/99f5bb36-4ef8-40cc-8ae5-e3bf1c7952ad
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.js
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 파일 다운로드 헬퍼
    const downloadZip = (blob, filename) => {
        const url = URL.createObjectURL(blob);
        if (typeof GM_download !== "undefined") {
            GM_download({
                url: url,
                name: filename,
                onload: () => URL.revokeObjectURL(url),
                onerror: () => {
                    const link = document.createElement("a");
                    link.href = url;
                    link.download = filename;
                    link.click();
                    URL.revokeObjectURL(url);
                }
            });
        } else {
            const link = document.createElement("a");
            link.href = url;
            link.download = filename;
            link.click();
            URL.revokeObjectURL(url);
        }
    };

    // 글로벌 이미지 저장소 (네트워크 모니터링용)
    window.capturedImageUrls = [];
    window.capturedImageBlobs = [];
    
    // fetch 인터셉트 (이미지 Blob 직접 캡처)
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
        const isImageUrl = url && (url.includes('.jpg') || url.includes('.png') || url.includes('.webp') || url.includes('.jpeg')) &&
            !url.includes('logo') && !url.includes('banner') && !url.includes('icon') && !url.includes('thumb');
        
        if (isImageUrl && !window.capturedImageUrls.includes(url)) {
            window.capturedImageUrls.push(url);
        }
        
        const result = originalFetch.apply(this, args);
        
        // Blob 응답 캡처 (Canvas Tainted 우회)
        if (isImageUrl && result instanceof Promise) {
            result.then(response => {
                if (response.ok && (response.headers.get('content-type')?.includes('image') || url.match(/\.(jpg|jpeg|png|webp)$/i))) {
                    const clonedResponse = response.clone();
                    clonedResponse.blob().then(blob => {
                        if (blob.size > 100 && !window.capturedImageBlobs.some(b => b.size === blob.size)) {
                            window.capturedImageBlobs.push(blob);
                        }
                    }).catch(() => {});
                }
                return response;
            }).catch(() => {});
        }
        
        return result;
    };

    // XMLHttpRequest 인터셉트 (arraybuffer 응답 캡처)
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function(method, url, ...rest) {
        this._captureUrl = url;
        return originalOpen.apply(this, [method, url, ...rest]);
    };
    XMLHttpRequest.prototype.send = function(...args) {
        const xhr = this;
        const originalOnreadystatechange = xhr.onreadystatechange;
        
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4 && xhr.status === 200) {
                const url = xhr._captureUrl;
                const isImageUrl = url && (url.includes('.jpg') || url.includes('.png') || url.includes('.webp') || url.includes('.jpeg')) &&
                    !url.includes('logo') && !url.includes('banner') && !url.includes('icon') && !url.includes('thumb');
                
                if (isImageUrl && xhr.responseType === 'arraybuffer') {
                    try {
                        const blob = new Blob([xhr.response], { type: 'image/jpeg' });
                        if (blob.size > 100 && !window.capturedImageBlobs.some(b => b.size === blob.size)) {
                            window.capturedImageBlobs.push(blob);
                        }
                    } catch (e) {}
                }
            }
            
            if (originalOnreadystatechange) {
                return originalOnreadystatechange.apply(this, arguments);
            }
        };
        return originalSend.apply(this, args);
    };

    // 현재 URL 체크
    const currentURL = document.URL;
    
    // ntk01이 아니면 스크립트 실행 안 함
    if (!currentURL.includes('ntk01.com')) {
        return;
    }

    // 페이지 타입 감지
    const pagePathParts = currentURL.split('/').filter(p => p);
    // URL을 더 정확히 분석 (https://ntk01.com/manhwa/11017/111796 형식)
    const pathMatch = currentURL.match(/\/manhwa\/(\d+)(?:\/(\d+))?/);
    const isEpisodePage = pathMatch && pathMatch[2] ? true : false; // 에피소드 번호가 있으면 에피소드 페이지
    const isListPage = pathMatch && !pathMatch[2] ? true : false; // 에피소드 번호가 없으면 목록 페이지
    
    // 도메인 추출
    const getProtocolDomain = () => {
        return currentURL.match(/^https?:\/\/[^:/?]+/)[0];
    };

    // 상대 URL을 절대 URL로 변환
    const resolveUrl = (url) => {
        if (!url) return '';
        if (url.startsWith('http://') || url.startsWith('https://')) {
            return url;
        }
        const protocolDomain = getProtocolDomain();
        if (url.startsWith('/')) {
            return protocolDomain + url;
        }
        // 상대주소 처리
        const currentPageUrl = currentURL.split('?')[0];
        const currentPath = currentPageUrl.substring(protocolDomain.length);
        const currentDir = currentPath.substring(0, currentPath.lastIndexOf('/') + 1);
        return protocolDomain + currentDir + url;
    };

    // 이미지 수집 함수 (에피소드 읽기 페이지)
    const collectImagesFromEpisodePage = () => {
        // ntk01.com 에피소드 페이지의 이미지 구조에 맞춘 선택자
        const imgElements = document.querySelectorAll('.vw-imgs img, .comic-viewer img, [class*="viewer"] img, .content img, .page img');
        
        imgElements.forEach(img => {
            const src = img.src || img.dataset.src || img.getAttribute('src');
            if (src) {
                const absoluteUrl = resolveUrl(src);
                if (absoluteUrl && !window.capturedImageUrls.includes(absoluteUrl)) {
                    window.capturedImageUrls.push(absoluteUrl);
                }
            }
        });
    };

    // 에피소드 페이지에서 이미지 추출
    const extractImagesFromEpisodeHtml = (html) => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const images = [];
        
        const imgElements = doc.querySelectorAll('.vw-imgs img, .comic-viewer img, [class*="viewer"] img, .content img, .page img');
        imgElements.forEach(img => {
            const src = img.src || img.dataset.src || img.getAttribute('src');
            if (src) {
                const absoluteUrl = resolveUrl(src);
                if (absoluteUrl && !images.includes(absoluteUrl)) {
                    images.push(absoluteUrl);
                }
            }
        });
        
        return images;
    };

    // 이미지 수집 함수 (목록 페이지)
    const collectImagesFromListPage = () => {
        // 목록 페이지의 썸네일/장편
        const imgElements = document.querySelectorAll('.list-item img, .episode img, .thumbnail img, [class*="thumb"] img');
        
        imgElements.forEach(img => {
            const src = img.src || img.dataset.src || img.getAttribute('src');
            if (src && !src.includes('logo') && !src.includes('banner') && !src.includes('icon')) {
                const absoluteUrl = resolveUrl(src);
                if (absoluteUrl && !window.capturedImageUrls.includes(absoluteUrl)) {
                    window.capturedImageUrls.push(absoluteUrl);
                }
            }
        });
    };

    // 이미지 수집 함수 (선택)
    const collectImagesFromDOM = () => {
        if (isEpisodePage) {
            collectImagesFromEpisodePage();
        } else if (isListPage) {
            collectImagesFromListPage();
        }
    };

    // comicinfo.xml 생성 함수
    const generateComicInfoXml = (title, seriesTitle = '', episodeNumber = '', writer = '', imageCount) => {
        const now = new Date();
        const dateStr = now.toISOString().split('T')[0];
        
        const comicInfo = `<?xml version="1.0" encoding="utf-8"?>
<ComicInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Title>${escapeXml(title)}</Title>
  <Series>${escapeXml(seriesTitle)}</Series>
  <Number>${escapeXml(episodeNumber)}</Number>
  <Count></Count>
  <Volume></Volume>
  <AlternateSeries></AlternateSeries>
  <AlternateNumber></AlternateNumber>
  <AlternateCount></AlternateCount>
  <Summary></Summary>
  <Notes></Notes>
  <Year></Year>
  <Month></Month>
  <Day></Day>
  <Writer>${escapeXml(writer)}</Writer>
  <Penciller></Penciller>
  <Inker></Inker>
  <Colorist></Colorist>
  <Letterer></Letterer>
  <CoverArtist></CoverArtist>
  <Publisher></Publisher>
  <Imprint></Imprint>
  <Genre></Genre>
  <Tags></Tags>
  <Web></Web>
  <PageCount>${imageCount}</PageCount>
  <LanguageISO>ko</LanguageISO>
  <Format>Manga</Format>
  <BlackAndWhite>No</BlackAndWhite>
  <Manga></Manga>
  <Characters></Characters>
  <Teams></Teams>
  <Locations></Locations>
  <ScanInformation></ScanInformation>
  <StoryArc></StoryArc>
  <SeriesGroup></SeriesGroup>
  <AgeRating>Unknown</AgeRating>
  <CommunityRating></CommunityRating>
  <MainCharacterOrTeam></MainCharacterOrTeam>
  <Review></Review>
</ComicInfo>`;
        
        return comicInfo;
    };

    // XML 특수문자 이스케이프 함수
    const escapeXml = (str) => {
        if (!str) return '';
        return str
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&apos;');
    };

    // 현재 페이지에서 시리즈 제목 추출
    const getSeriesTitle = () => {
        const workEl = document.querySelector('.vw-work');
        return workEl ? workEl.textContent.trim() : '';
    };

    // 현재 페이지에서 에피소드 제목 추출
    const getEpisodeTitle = () => {
        const epEl = document.querySelector('.vw-ep');
        if (!epEl) return '';

        const span = epEl.querySelector('span');
        const episodeTitle = span ? span.textContent.trim().replace(/^[\s\-]+/, '') : '';
        return episodeTitle;
    };

    const GetEpisodeNumberFromTitle = (title) => {
        const epEl = document.querySelector('.vw-ep');
        if (!epEl) return '';

        const strong = epEl.querySelector('strong');
        const episodeNum = strong ? strong.textContent.trim() : '';
        return episodeNum;
    };

    // HTML에서 시리즈 제목 추출
    const getSeriesTitleFromHtml = (html) => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const workEl = doc.querySelector('.vw-work');
        return workEl ? workEl.textContent.trim() : '';
    };

    // HTML에서 에피소드 제목 추출
    const getEpisodeTitleFromHtml = (html) => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const epEl = doc.querySelector('.vw-ep');
        if (!epEl) return '';

        const span = epEl.querySelector('span');
        const episodeTitle = span ? span.textContent.trim().replace(/^[\s\-]+/, '') : '';
        return episodeTitle;
    };

        // HTML에서 에피소드 번호 추출
    const getEpisodeNumFromHtml = (html) => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const epEl = doc.querySelector('.vw-ep');
        if (!epEl) return '';

        const strong = epEl.querySelector('strong');
        const episodeNum = strong ? strong.textContent.trim() : '';
        return episodeNum;
    };

    // HTML에서 작가 정보 추출
    const getWriterFromHtml = (html) => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        
        // 방법 1: .vw-info 또는 유사 정보 영역에서 찾기
        let writerEl = doc.querySelector('[class*="info"] span:contains("작가")') ||
                       doc.querySelector('.vw-info');
        
        // 방법 2: "작가"라는 텍스트 다음에 오는 요소 찾기
        if (!writerEl) {
            const allSpans = doc.querySelectorAll('span, div, p');
            for (let el of allSpans) {
                if (el.textContent && el.textContent.includes('작가')) {
                    const match = el.textContent.match(/작가[\s:]+([^\n,]+)/);
                    if (match && match[1]) {
                        return match[1].trim();
                    }
                }
            }
        }
        
        // 방법 3: data-author 속성 확인
        const authorAttr = doc.querySelector('[data-author]');
        if (authorAttr) {
            return authorAttr.getAttribute('data-author').trim();
        }
        
        // 방법 4: 작가 메타데이터 태그 확인
        const metaAuthor = doc.querySelector('meta[name="author"]');
        if (metaAuthor) {
            return metaAuthor.getAttribute('content').trim();
        }
        
        return '';
    };

    // 페이지 로드 완료 후 이미지 수집
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', collectImagesFromDOM);
    } else {
        collectImagesFromDOM();
    }

    // MutationObserver를 통한 동적 이미지 감지
    const observer = new MutationObserver(() => {
        collectImagesFromDOM();
    });
    
    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['src']
    });

    // Canvas를 사용해 DOM의 img 태그에서 직접 blob 추출 (가장 안정적)
    const extractImageBlobFromImg = (imgElement) => {
        return new Promise((resolve, reject) => {
            try {
                // 이미지가 실제로 로드되었는지 확인
                if (!imgElement.complete || imgElement.naturalWidth === 0) {
                    // 아직 로드 중이면 onload 대기
                    const tempImg = new Image();
                    tempImg.crossOrigin = 'anonymous';
                    tempImg.onload = () => {
                        try {
                            const canvas = document.createElement('canvas');
                            canvas.width = tempImg.naturalWidth;
                            canvas.height = tempImg.naturalHeight;
                            const ctx = canvas.getContext('2d');
                            ctx.drawImage(tempImg, 0, 0);
                            canvas.toBlob(blob => {
                                if (blob && blob.size > 0) {
                                    resolve(blob);
                                } else {
                                    reject(new Error('Canvas toBlob failed'));
                                }
                            }, 'image/jpeg', 0.95);
                        } catch (e) {
                            reject(e);
                        }
                    };
                    tempImg.onerror = () => reject(new Error('Image load failed'));
                    tempImg.src = imgElement.src || imgElement.dataset.src;
                } else {
                    // 이미 로드됨
                    const canvas = document.createElement('canvas');
                    canvas.width = imgElement.naturalWidth || imgElement.width;
                    canvas.height = imgElement.naturalHeight || imgElement.height;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(imgElement, 0, 0);
                    canvas.toBlob(blob => {
                        if (blob && blob.size > 0) {
                            resolve(blob);
                        } else {
                            reject(new Error('Canvas toBlob failed'));
                        }
                    }, 'image/jpeg', 0.95);
                }
            } catch (error) {
                reject(error);
            }
        });
    };

    // GM_xmlhttpRequest를 이용한 이미지 다운로드 (Canvas 실패 시 대체)
    const downloadImageViaGM = (url) => {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                onload: (response) => {
                    if (response.status === 200 && response.response) {
                        resolve(response.response);
                    } else {
                        reject(new Error(`HTTP ${response.status}`));
                    }
                },
                onerror: (error) => {
                    reject(new Error(`Network error: ${error}`));
                }
            });
        });
    };

    // 다운로드 함수 (개별 에피소드)
    const downloadImages = async () => {
        // DOM에서 직접 img 태그 수집 (가장 확실한 방법)
        const imgElements = document.querySelectorAll('.vw-imgs img, .comic-viewer img, [class*="viewer"] img, .content img, .page img');
        
        if (imgElements.length === 0) {
            alert('다운로드할 이미지를 찾을 수 없습니다.');
            return;
        }

        // 제목 추출
        let title = getEpisodeTitle() || document.title.split('|')[0].trim() || 'ntk01_download';
        title = title.replace(/[/\\:*?"<>|]/g, '_');

        console.log(`발견된 이미지: ${imgElements.length}개`);

        const zip = new JSZip();
        let imageCount = 0;
        let failedCount = 0;

        // 각 img 태그에서 Canvas로 blob 추출
        for (let i = 0; i < imgElements.length; i++) {
            const imgElement = imgElements[i];
            try {
                const blob = await extractImageBlobFromImg(imgElement);
                if (blob && blob.size > 0) {
                    const filename = `${String(imageCount).padStart(4, '0')}_page_${i + 1}.jpg`;
                    zip.file(filename, blob);
                    imageCount++;
                }
            } catch (error) {
                console.warn(`이미지 추출 실패 (${i + 1}번째):`, error);
                failedCount++;
            }
        }

        // ZIP 생성 및 다운로드
        if (imageCount > 0) {
            const seriesTitle = getSeriesTitle();
            const writer = getWriterFromHtml(document.documentElement.outerHTML);
            const comicInfoXml = generateComicInfoXml(title, imageCount, seriesTitle, '', writer);
            zip.file('ComicInfo.xml', comicInfoXml);
            
            const blob = await zip.generateAsync({ type: 'blob' });
            downloadZip(blob, `${title}.zip`);
            alert(`✓ ${imageCount}개 이미지 + ComicInfo.xml 다운로드 완료\n(실패: ${failedCount}개)`);
        } else {
            alert(`❌ 다운로드 가능한 이미지 없음 (실패: ${failedCount}개)`);
        }
    };

    // 모든 에피소드를 각각 ZIP으로 다운로드 (목록 페이지용)
    const downloadAllEpisodesAsZips = async () => {
        // 에피소드 링크 찾기
        const episodeElements = document.querySelectorAll('a.ep-row-v2-link');
        
        if (episodeElements.length === 0) {
            alert('다운로드할 에피소드를 찾을 수 없습니다.');
            return;
        }

        // 목록 페이지에서 시리즈 정보 추출 (모든 챕터에서 공통)
        const seriesTitle = getSeriesTitle();
        const writerFromPage = document.querySelector('.hero-v2-author');
        const seriesWriter = writerFromPage ? writerFromPage.textContent.trim() : '';

        const episodeLinks = Array.from(episodeElements).map((a, index) => {
            const titleElement = a.querySelector('.ep-row-v2-title strong');
            const title = titleElement ? titleElement.textContent.trim() : a.textContent.trim();
            return { url: a.href, title, orderNumber: index + 1 };
        });

        console.log(`${episodeLinks.length}개의 에피소드 발견`);
        console.log(`시리즈: ${seriesTitle}, 작가: ${seriesWriter}`);

        let downloadedCount = 0;
        let failedCount = 0;

        for (let i = 0; i < episodeLinks.length; i++) {
            const episode = episodeLinks[i];
            try {
                console.log(`[${i + 1}/${episodeLinks.length}] 다운로드 중: ${episode.title}`);

                // iframe에 에피소드 로드 (실제 렌더링 필요)
                const iframe = document.createElement('iframe');
                iframe.style.display = 'none';
                iframe.src = episode.url;
                document.body.appendChild(iframe);

                // iframe 로드 대기
                await new Promise((resolve) => {
                    const checkLoad = () => {
                        try {
                            if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
                                resolve();
                            } else {
                                setTimeout(checkLoad, 100);
                            }
                        } catch (e) {
                            // CORS 오류 발생 - iframe 방식 사용 불가
                            resolve();
                        }
                    };
                    setTimeout(checkLoad, 500);
                });

                let imageCount = 0;
                let imgFailedCount = 0;
                let chapterTitle = episode.title;

                try {
                    // iframe 내 이미지 추출 시도
                    const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                    const imgElements = iframeDoc.querySelectorAll('.vw-imgs img');
                    
                    // iframe에서 챕터 제목 추출 (더 정확함)
                    const iframeEpisodeTitle = iframeDoc.querySelector('.vw-ep');
                    if (iframeEpisodeTitle) {
                        const strong = iframeEpisodeTitle.querySelector('strong');
                        const span = iframeEpisodeTitle.querySelector('span');
                        const episodeNum = strong ? strong.textContent.trim() : '';
                        const episodeSubtitle = span ? span.textContent.trim().replace(/^[\s\-]+/, '') : '';
                        chapterTitle = episodeNum ? `${episodeNum} - ${episodeSubtitle}` : episodeSubtitle;
                        
                    }
                    
                    if (imgElements.length > 0) {
                        const zip = new JSZip();
                        
                        for (let j = 0; j < imgElements.length; j++) {
                            try {
                                const blob = await extractImageBlobFromImg(imgElements[j]);
                                if (blob && blob.size > 0) {
                                    const filename = `${String(imageCount).padStart(4, '0')}_page_${j + 1}.jpg`;
                                    zip.file(filename, blob);
                                    imageCount++;
                                }
                            } catch (error) {
                                imgFailedCount++;
                            }
                        }

                        if (imageCount > 0) {
                            // 파일명: "01 - 챕터제목.zip" 형식
                            const cleanTitle = chapterTitle.replace(/[/\\:*?"<>|]/g, '_').slice(0, 50);
                            const filename = `${cleanTitle}.zip`;
                            
                            
                            // comicinfo.xml 생성 (인덱스 번호, 시리즈, 작가 포함)
                            const comicInfoXml = generateComicInfoXml(
                                cleanTitle,          // title
                                imageCount,          // pageCount
                                seriesTitle,         // series
                                episode.orderNumber.toString(),  // number (인덱스)
                                seriesWriter         // writer
                            );
                            zip.file('ComicInfo.xml', comicInfoXml);
                            
                            const blob = await zip.generateAsync({ type: 'blob' });
                            downloadZip(blob, filename);
                            downloadedCount++;
                            console.log(`✓ ${filename} (${imageCount}개 이미지)`);
                        }
                    }
                } catch (error) {
                    console.warn(`iframe 접근 실패:`, error);
                }

                iframe.remove();

                if (imageCount === 0) {
                    failedCount++;
                }

                // 레이트 제한
                await new Promise(resolve => setTimeout(resolve, 1000));
            } catch (error) {
                console.error(`에피소드 처리 실패: ${episode.title}`, error);
                failedCount++;
            }
        }

        alert(`완료!\n다운로드: ${downloadedCount}개\n실패: ${failedCount}개`);
    };

    // 메뉴 명령 등록 (페이지 타입에 따라 다른 기능)
    if (isEpisodePage) {
        GM_registerMenuCommand('📥 현재 에피소드 다운로드', downloadImages);
    } else if (isListPage) {
        GM_registerMenuCommand('📥 전체 에피소드 다운로드', downloadAllEpisodesAsZips);
    }

    });

    console.log(`ntk01 Downloader 스크립트 로드 완료 (${isEpisodePage ? '에피소드 페이지' : isListPage ? '목록 페이지' : '기타'})`);
})();
