from __future__ import annotations

import re
import ssl
import socket
import time
import xml.etree.ElementTree as ET
from dataclasses import asdict, dataclass
from html.parser import HTMLParser
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
from urllib.error import HTTPError, URLError
from urllib.parse import urljoin, urlparse, urldefrag
from urllib.request import Request, urlopen

APP_USER_AGENT = "TamerSEOAnalyzer/1.4.2 (+https://tamerserbes.com)"
MAX_HTML_BYTES = 5 * 1024 * 1024
MAX_SITEMAP_BYTES = 10 * 1024 * 1024
ProgressCallback = Optional[Callable[[str], None]]


@dataclass
class FetchResult:
    url: str
    final_url: str
    status_code: int
    headers: Dict[str, str]
    html: str
    raw_size: int
    load_ms: int
    error: Optional[str] = None


class SEOHTMLParser(HTMLParser):
    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.title_parts: List[str] = []
        self.in_title = False
        self.meta_tags: List[Dict[str, str]] = []
        self.links: List[Dict[str, str]] = []
        self.images: List[Dict[str, str]] = []
        self.canonical: Optional[str] = None
        self.h1: List[str] = []
        self.h2: List[str] = []
        self.h3: List[str] = []
        self.current_heading: Optional[str] = None
        self.heading_buffer: List[str] = []
        self.ld_json_blocks: List[str] = []
        self.html_lang = ""
        self.in_ld_json = False
        self.script_buffer: List[str] = []

    @staticmethod
    def _attrs(attrs: List[Tuple[str, Optional[str]]]) -> Dict[str, str]:
        return {str(k).lower(): (v or "") for k, v in attrs}

    def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
        tag = tag.lower()
        attr = self._attrs(attrs)
        if tag == "html":
            self.html_lang = attr.get("lang", "").strip()
        elif tag == "title":
            self.in_title = True
        elif tag == "meta":
            self.meta_tags.append(attr)
        elif tag == "link":
            rel = attr.get("rel", "").lower()
            href = attr.get("href", "")
            if "canonical" in rel and href:
                self.canonical = href
        elif tag == "a":
            href = attr.get("href", "")
            if href:
                self.links.append({"href": href, "text": ""})
        elif tag == "img":
            self.images.append({
                "src": attr.get("src", ""),
                "alt": attr.get("alt", ""),
                "width": attr.get("width", ""),
                "height": attr.get("height", ""),
                "loading": attr.get("loading", ""),
            })
        elif tag in {"h1", "h2", "h3"}:
            self.current_heading = tag
            self.heading_buffer = []
        elif tag == "script":
            script_type = attr.get("type", "").lower().strip()
            if "application/ld+json" in script_type:
                self.in_ld_json = True
                self.script_buffer = []

    def handle_data(self, data: str) -> None:
        if self.in_title:
            self.title_parts.append(data)
        if self.current_heading:
            self.heading_buffer.append(data)
        if self.in_ld_json:
            self.script_buffer.append(data)

    def handle_endtag(self, tag: str) -> None:
        tag = tag.lower()
        if tag == "title":
            self.in_title = False
        elif tag in {"h1", "h2", "h3"} and self.current_heading == tag:
            text = clean_text(" ".join(self.heading_buffer))
            if text:
                if tag == "h1":
                    self.h1.append(text)
                elif tag == "h2":
                    self.h2.append(text)
                elif tag == "h3":
                    self.h3.append(text)
            self.current_heading = None
            self.heading_buffer = []
        elif tag == "script" and self.in_ld_json:
            block = "".join(self.script_buffer).strip()
            if block:
                self.ld_json_blocks.append(block)
            self.in_ld_json = False
            self.script_buffer = []

    @property
    def title(self) -> str:
        return clean_text(" ".join(self.title_parts))

    def meta_content(self, key: str, value: str) -> Optional[str]:
        key = key.lower()
        value = value.lower()
        for meta in self.meta_tags:
            if meta.get(key, "").lower() == value:
                return meta.get("content", "").strip()
        return None

    @property
    def meta_description(self) -> str:
        return self.meta_content("name", "description") or ""

    @property
    def viewport(self) -> str:
        return self.meta_content("name", "viewport") or ""

    @property
    def robots_content(self) -> str:
        return self.meta_content("name", "robots") or ""

    @property
    def charset(self) -> str:
        for meta in self.meta_tags:
            if meta.get("charset"):
                return meta["charset"]
        return ""

    @property
    def og_count(self) -> int:
        return sum(1 for meta in self.meta_tags if meta.get("property", "").lower().startswith("og:"))


def clean_text(value: str) -> str:
    return re.sub(r"\s+", " ", value or "").strip()


def normalize_url(url: str) -> str:
    url = (url or "").strip()
    if not url:
        raise ValueError("URL boş olamaz.")
    if not re.match(r"^https?://", url, re.I):
        url = "https://" + url
    parsed = urlparse(url)
    if not parsed.netloc:
        raise ValueError("Geçerli bir site adresi girin. Örnek: https://tamerserbes.com")
    return url


def normalize_page_url(url: str) -> str:
    url, _frag = urldefrag(url)
    parsed = urlparse(url)
    path = parsed.path or "/"
    # Normalize only empty path. Do not remove normal trailing slash, because some sites use it.
    return parsed._replace(path=path).geturl()


def same_domain(url_a: str, url_b: str) -> bool:
    return urlparse(url_a).netloc.lower().replace("www.", "") == urlparse(url_b).netloc.lower().replace("www.", "")


def detect_charset(headers: Dict[str, str]) -> str:
    content_type = headers.get("content-type", "")
    match = re.search(r"charset=([^;]+)", content_type, re.I)
    if match:
        return match.group(1).strip()
    return "utf-8"


def fetch_url(url: str, timeout: int = 15, accept: str = "text/html,application/xhtml+xml,*/*") -> FetchResult:
    normalized = normalize_url(url)
    request = Request(normalized, headers={"User-Agent": APP_USER_AGENT, "Accept": accept})
    started = time.perf_counter()
    try:
        with urlopen(request, timeout=timeout) as response:
            raw = response.read(MAX_HTML_BYTES + 1)
            load_ms = int((time.perf_counter() - started) * 1000)
            headers = {k.lower(): v for k, v in response.headers.items()}
            charset = detect_charset(headers)
            html = raw[:MAX_HTML_BYTES].decode(charset, errors="replace")
            return FetchResult(normalized, response.geturl(), getattr(response, "status", 200), headers, html, len(raw), load_ms)
    except HTTPError as exc:
        raw = exc.read(1024 * 512)
        load_ms = int((time.perf_counter() - started) * 1000)
        headers = {k.lower(): v for k, v in exc.headers.items()}
        html = raw.decode(detect_charset(headers), errors="replace") if raw else ""
        return FetchResult(normalized, normalized, exc.code, headers, html, len(raw), load_ms, str(exc))
    except (URLError, TimeoutError, socket.timeout, ssl.SSLError, ValueError) as exc:
        load_ms = int((time.perf_counter() - started) * 1000)
        return FetchResult(normalized, normalized, 0, {}, "", 0, load_ms, str(exc))


def quick_status(url: str, timeout: int = 8) -> Tuple[int, Optional[str]]:
    try:
        request = Request(url, method="HEAD", headers={"User-Agent": APP_USER_AGENT})
        with urlopen(request, timeout=timeout) as response:
            return getattr(response, "status", 200), None
    except HTTPError as exc:
        if exc.code in {405, 403}:
            try:
                request = Request(url, method="GET", headers={"User-Agent": APP_USER_AGENT})
                with urlopen(request, timeout=timeout) as response:
                    response.read(512)
                    return getattr(response, "status", 200), None
            except Exception as inner:  # noqa: BLE001
                return getattr(inner, "code", 0), str(inner)
        return exc.code, str(exc)
    except Exception as exc:  # noqa: BLE001
        return 0, str(exc)


def check_file_exists(base_url: str, path: str) -> Dict[str, Any]:
    parsed = urlparse(base_url)
    target = f"{parsed.scheme}://{parsed.netloc}{path}"
    status, error = quick_status(target)
    return {"url": target, "status_code": status, "exists": 200 <= status < 400, "error": error}


def absolute_links(base_url: str, hrefs: List[Dict[str, str]]) -> List[str]:
    links: List[str] = []
    seen = set()
    for item in hrefs:
        href = (item.get("href") or "").strip()
        if not href or href.startswith(("mailto:", "tel:", "javascript:", "#")):
            continue
        abs_url = normalize_page_url(urljoin(base_url, href))
        parsed = urlparse(abs_url)
        if parsed.scheme not in {"http", "https"} or not parsed.netloc:
            continue
        if abs_url not in seen:
            seen.add(abs_url)
            links.append(abs_url)
    return links


def internal_links(base_url: str, links: Iterable[str]) -> List[str]:
    result: List[str] = []
    seen = set()
    for link in links:
        parsed = urlparse(link)
        if parsed.scheme in {"http", "https"} and same_domain(base_url, link):
            if not re.search(r"\.(jpg|jpeg|png|gif|svg|webp|pdf|zip|rar|7z|mp4|mp3|avi|css|js|ico)$", parsed.path, re.I):
                if link not in seen:
                    seen.add(link)
                    result.append(link)
    return result


def check_links(links: List[str], limit: int = 20) -> Dict[str, Any]:
    checked: List[Dict[str, Any]] = []
    for link in links[:limit]:
        status, error = quick_status(link, timeout=8)
        checked.append({"url": link, "status_code": status, "ok": 200 <= status < 400, "error": error})
    broken = [item for item in checked if not item["ok"]]
    return {"checked_count": len(checked), "total_links": len(links), "broken_count": len(broken), "broken_links": broken, "checked_links": checked}


def status_label(status: str) -> str:
    return {"success": "Başarılı", "warning": "Uyarı", "error": "Hata", "info": "Bilgi"}.get(status, status)


def add_check(checks: List[Dict[str, Any]], category: str, name: str, status: str, score: int, max_score: int, detail: str, recommendation: str) -> None:
    checks.append({"category": category, "name": name, "status": status, "status_label": status_label(status), "score": max(0, min(score, max_score)), "max_score": max_score, "detail": detail, "recommendation": recommendation})


def grade_for_score(score: int) -> str:
    if score >= 90:
        return "A"
    if score >= 80:
        return "B"
    if score >= 70:
        return "C"
    if score >= 55:
        return "D"
    return "E"


def analyze_url(url: str, check_link_health: bool = True, max_links_to_check: int = 20) -> Dict[str, Any]:
    fetch = fetch_url(url)
    if fetch.error and not fetch.html:
        return {"ok": False, "error": fetch.error, "url": url, "final_url": fetch.final_url, "status_code": fetch.status_code, "score": 0, "grade": "E", "checks": [], "created_at": time.strftime("%Y-%m-%d %H:%M:%S")}

    parser = SEOHTMLParser()
    parser.feed(fetch.html or "")
    parser.close()

    final_url = fetch.final_url or fetch.url
    parsed_final = urlparse(final_url)
    base_site = f"{parsed_final.scheme}://{parsed_final.netloc}"
    links = absolute_links(final_url, parser.links)
    internal = internal_links(final_url, links)
    external = [link for link in links if link not in internal]
    robots = check_file_exists(final_url, "/robots.txt")
    sitemap = check_file_exists(final_url, "/sitemap.xml")

    link_report = check_links(links, max_links_to_check) if check_link_health else None
    checks: List[Dict[str, Any]] = []

    title = parser.title
    title_len = len(title)
    if 10 <= title_len <= 65:
        add_check(checks, "Temel SEO", "Title etiketi", "success", 10, 10, f"Title mevcut: {title_len} karakter.", "Title uzunluğu iyi görünüyor.")
    elif title:
        add_check(checks, "Temel SEO", "Title etiketi", "warning", 6, 10, f"Title mevcut ama uzunluk önerilen aralıkta değil: {title_len} karakter.", "Title için yaklaşık 10-65 karakter aralığı hedeflenebilir.")
    else:
        add_check(checks, "Temel SEO", "Title etiketi", "error", 0, 10, "Sayfada title etiketi bulunamadı.", "Her sayfaya açıklayıcı ve benzersiz title ekleyin.")

    description = parser.meta_description
    desc_len = len(description)
    if 50 <= desc_len <= 160:
        add_check(checks, "Temel SEO", "Meta description", "success", 10, 10, f"Meta description mevcut: {desc_len} karakter.", "Açıklama uzunluğu iyi görünüyor.")
    elif description:
        add_check(checks, "Temel SEO", "Meta description", "warning", 6, 10, f"Meta description mevcut ama uzunluk zayıf: {desc_len} karakter.", "Sayfayı özetleyen 50-160 karakterlik açıklama yazın.")
    else:
        add_check(checks, "Temel SEO", "Meta description", "error", 0, 10, "Meta description bulunamadı.", "Sayfanın içeriğini özetleyen description etiketi ekleyin.")

    lang_value = clean_text(parser.html_lang)
    if lang_value:
        add_check(checks, "Teknik", "HTML lang etiketi", "success", 3, 3, f"HTML lang değeri mevcut: {lang_value}", "Dil etiketi doğru görünüyor.")
    else:
        add_check(checks, "Teknik", "HTML lang etiketi", "warning", 1, 3, "HTML lang değeri bulunamadı.", "<html lang='tr'> gibi doğru dil etiketi ekleyin.")

    charset_value = parser.charset or detect_charset(fetch.headers)
    if charset_value:
        add_check(checks, "Teknik", "Charset", "success", 3, 3, f"Karakter seti bulundu: {charset_value}", "Karakter seti mevcut.")
    else:
        add_check(checks, "Teknik", "Charset", "warning", 1, 3, "Karakter seti belirlenemedi.", "<meta charset='utf-8'> kullanın.")

    robots_meta = clean_text(parser.robots_content)
    x_robots = clean_text(fetch.headers.get("x-robots-tag", ""))
    robots_text = f"{robots_meta} {x_robots}".lower()
    if "noindex" in robots_text:
        add_check(checks, "Index", "Indexlenebilirlik", "error", 0, 8, f"noindex algılandı. Meta: {robots_meta or '-'} X-Robots: {x_robots or '-'}", "Arama sonuçlarında görünmesini istediğiniz sayfalarda noindex kullanmayın.")
    else:
        add_check(checks, "Index", "Indexlenebilirlik", "success", 8, 8, "noindex yönergesi bulunmadı.", "Sayfanın indexlenebilirliği iyi görünüyor.")

    h1_count = len(parser.h1)
    if h1_count == 1:
        add_check(checks, "İçerik", "H1 başlığı", "success", 10, 10, f"1 adet H1 bulundu: {parser.h1[0]}", "Ana başlık yapısı iyi.")
    elif h1_count > 1:
        add_check(checks, "İçerik", "H1 başlığı", "warning", 6, 10, f"{h1_count} adet H1 bulundu.", "Genellikle sayfada tek ve net bir H1 kullanın.")
    else:
        add_check(checks, "İçerik", "H1 başlığı", "error", 0, 10, "H1 başlığı bulunamadı.", "Sayfanın ana konusunu anlatan bir H1 ekleyin.")

    if len(parser.h2) > 0:
        add_check(checks, "İçerik", "H2 başlıkları", "success", 5, 5, f"{len(parser.h2)} adet H2 bulundu.", "Alt başlık yapısı mevcut.")
    else:
        add_check(checks, "İçerik", "H2 başlıkları", "warning", 2, 5, "H2 başlığı bulunamadı.", "İçeriği bölümlere ayırmak için H2 başlıkları kullanın.")

    if parsed_final.scheme == "https":
        add_check(checks, "Teknik", "HTTPS kullanımı", "success", 10, 10, "Site HTTPS üzerinden açılıyor.", "Güvenli bağlantı aktif.")
    else:
        add_check(checks, "Teknik", "HTTPS kullanımı", "error", 0, 10, "Site HTTP üzerinden açılıyor.", "SSL sertifikası kurup HTTPS yönlendirmesi yapın.")

    if parser.canonical:
        add_check(checks, "Teknik", "Canonical etiketi", "success", 5, 5, f"Canonical bulundu: {urljoin(final_url, parser.canonical)}", "Canonical yapı mevcut.")
    else:
        add_check(checks, "Teknik", "Canonical etiketi", "warning", 2, 5, "Canonical etiketi bulunamadı.", "Kopya URL riskini azaltmak için canonical etiketi ekleyin.")

    if parser.viewport:
        add_check(checks, "Mobil", "Viewport etiketi", "success", 5, 5, "Viewport meta etiketi mevcut.", "Mobil görünüm için temel etiket mevcut.")
    else:
        add_check(checks, "Mobil", "Viewport etiketi", "error", 0, 5, "Viewport meta etiketi bulunamadı.", "Mobil uyum için <meta name='viewport' content='width=device-width, initial-scale=1'> ekleyin.")

    img_count = len(parser.images)
    missing_alt = [img for img in parser.images if not clean_text(img.get("alt", ""))]
    if img_count == 0:
        add_check(checks, "Görsel SEO", "Görsel alt etiketleri", "success", 10, 10, "Sayfada görsel bulunamadı.", "Görsel yoksa alt etiketi sorunu oluşmaz.")
    else:
        alt_ratio = int(((img_count - len(missing_alt)) / img_count) * 100)
        if alt_ratio >= 90:
            add_check(checks, "Görsel SEO", "Görsel alt etiketleri", "success", 10, 10, f"{img_count} görselin %{alt_ratio} oranında alt etiketi var.", "Görsel açıklamaları iyi durumda.")
        elif alt_ratio >= 50:
            add_check(checks, "Görsel SEO", "Görsel alt etiketleri", "warning", 6, 10, f"{img_count} görselden {len(missing_alt)} tanesinde alt etiketi eksik.", "Eksik görsellere açıklayıcı alt metin ekleyin.")
        else:
            add_check(checks, "Görsel SEO", "Görsel alt etiketleri", "error", 2, 10, f"{img_count} görselden {len(missing_alt)} tanesinde alt etiketi eksik.", "Görsellerin çoğuna anlamlı alt metin ekleyin.")

    if robots.get("exists"):
        add_check(checks, "Teknik", "robots.txt", "success", 5, 5, f"robots.txt bulundu: {robots['url']}", "Arama motoru yönlendirme dosyası mevcut.")
    else:
        add_check(checks, "Teknik", "robots.txt", "warning", 1, 5, "robots.txt bulunamadı veya erişilemedi.", "Site kök dizinine robots.txt ekleyin.")

    if sitemap.get("exists"):
        add_check(checks, "Teknik", "sitemap.xml", "success", 5, 5, f"sitemap.xml bulundu: {sitemap['url']}", "Site haritası mevcut.")
    else:
        add_check(checks, "Teknik", "sitemap.xml", "warning", 1, 5, "sitemap.xml bulunamadı veya erişilemedi.", "Arama motorlarının sayfaları keşfetmesi için sitemap.xml ekleyin.")

    if parser.ld_json_blocks:
        add_check(checks, "Zengin Sonuç", "JSON-LD structured data", "success", 4, 4, f"{len(parser.ld_json_blocks)} adet JSON-LD bloğu bulundu.", "Yapılandırılmış veri mevcut.")
    else:
        add_check(checks, "Zengin Sonuç", "JSON-LD structured data", "warning", 1, 4, "JSON-LD structured data bulunamadı.", "Uygun sayfalara Organization, Article, Product veya Breadcrumb schema eklenebilir.")

    if parser.og_count >= 3:
        add_check(checks, "Sosyal SEO", "Open Graph", "success", 4, 4, f"{parser.og_count} adet Open Graph etiketi bulundu.", "Sosyal medya paylaşım görünümü iyi.")
    elif parser.og_count > 0:
        add_check(checks, "Sosyal SEO", "Open Graph", "warning", 2, 4, f"{parser.og_count} adet Open Graph etiketi bulundu.", "og:title, og:description, og:image ve og:url etiketlerini tamamlayın.")
    else:
        add_check(checks, "Sosyal SEO", "Open Graph", "warning", 1, 4, "Open Graph etiketi bulunamadı.", "Sosyal medya paylaşım kartları için Open Graph etiketleri ekleyin.")

    if fetch.load_ms <= 2000:
        add_check(checks, "Performans", "Yanıt süresi", "success", 5, 5, f"Sayfa {fetch.load_ms} ms içinde yanıt verdi.", "Yanıt süresi iyi görünüyor.")
    elif fetch.load_ms <= 5000:
        add_check(checks, "Performans", "Yanıt süresi", "warning", 3, 5, f"Sayfa yanıtı orta seviye: {fetch.load_ms} ms.", "Görsel, cache ve hosting performansını gözden geçirin.")
    else:
        add_check(checks, "Performans", "Yanıt süresi", "error", 1, 5, f"Sayfa yavaş yanıt verdi: {fetch.load_ms} ms.", "Sunucu, cache ve dosya optimizasyonu yapılmalı.")

    page_kb = int(fetch.raw_size / 1024)
    if page_kb <= 1024:
        add_check(checks, "Performans", "HTML boyutu", "success", 3, 3, f"HTML boyutu yaklaşık {page_kb} KB.", "HTML boyutu iyi görünüyor.")
    else:
        add_check(checks, "Performans", "HTML boyutu", "warning", 1, 3, f"HTML boyutu yüksek: {page_kb} KB.", "Gereksiz HTML, inline CSS/JS ve büyük veri bloklarını azaltın.")

    if link_report:
        broken = int(link_report.get("broken_count", 0))
        if broken == 0:
            add_check(checks, "Link", "Kırık link kontrolü", "success", 5, 5, f"{link_report.get('checked_count')} link kontrol edildi, sorun bulunmadı.", "Linkler iyi görünüyor.")
        elif broken <= 2:
            add_check(checks, "Link", "Kırık link kontrolü", "warning", 3, 5, f"{broken} sorunlu link bulundu.", "Sorunlu linkleri güncelleyin veya kaldırın.")
        else:
            add_check(checks, "Link", "Kırık link kontrolü", "error", 1, 5, f"{broken} sorunlu link bulundu.", "Kırık linkler kullanıcı deneyimini ve SEO kalitesini düşürür.")

    max_score = sum(item["max_score"] for item in checks) or 1
    total_score = sum(item["score"] for item in checks)
    score = int(round((total_score / max_score) * 100))

    return {
        "ok": True,
        "url": fetch.url,
        "final_url": final_url,
        "base_site": base_site,
        "status_code": fetch.status_code,
        "headers": fetch.headers,
        "load_ms": fetch.load_ms,
        "raw_size": fetch.raw_size,
        "score": score,
        "grade": grade_for_score(score),
        "checks": checks,
        "title": title,
        "meta_description": description,
        "h1": parser.h1,
        "h2": parser.h2,
        "h3": parser.h3,
        "canonical": urljoin(final_url, parser.canonical) if parser.canonical else "",
        "html_lang": lang_value,
        "charset": charset_value,
        "robots_meta": robots_meta,
        "x_robots_tag": x_robots,
        "viewport": parser.viewport,
        "og_count": parser.og_count,
        "ld_json_count": len(parser.ld_json_blocks),
        "images": parser.images,
        "missing_alt_images": missing_alt,
        "links": links,
        "internal_links": internal,
        "external_links": external,
        "link_report": link_report,
        "robots_txt": robots,
        "sitemap_xml": sitemap,
        "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
    }


def _parse_sitemap_url(sitemap_url: str, site_root: str, limit: int = 50, depth: int = 0) -> List[str]:
    if depth > 2 or limit <= 0:
        return []
    fetch = fetch_url(sitemap_url, timeout=12, accept="application/xml,text/xml,*/*")
    if fetch.status_code < 200 or fetch.status_code >= 400 or not fetch.html:
        return []
    xml_text = fetch.html[:MAX_SITEMAP_BYTES]
    urls: List[str] = []
    try:
        root = ET.fromstring(xml_text)
        locs = [clean_text(node.text or "") for node in root.iter() if node.tag.lower().endswith("loc")]
    except Exception:
        locs = [clean_text(match) for match in re.findall(r"<loc>\s*([^<]+)\s*</loc>", xml_text, flags=re.I)]

    for loc in locs:
        if not loc:
            continue
        loc = normalize_page_url(loc)
        if loc.lower().endswith(".xml") and same_domain(site_root, loc):
            for nested in _parse_sitemap_url(loc, site_root, limit=limit - len(urls), depth=depth + 1):
                if nested not in urls:
                    urls.append(nested)
        elif same_domain(site_root, loc) and loc not in urls:
            urls.append(loc)
        if len(urls) >= limit:
            break
    return urls[:limit]


def fetch_sitemap_urls(base_url: str, limit: int = 50, depth: int = 0) -> List[str]:
    parsed = urlparse(normalize_url(base_url))
    site_root = f"{parsed.scheme}://{parsed.netloc}"
    sitemap_url = f"{site_root}/sitemap.xml"
    return _parse_sitemap_url(sitemap_url, site_root, limit=limit, depth=depth)


def discover_urls(base_url: str, max_pages: int = 10, use_sitemap: bool = True, progress: ProgressCallback = None) -> List[str]:
    start_url = normalize_page_url(normalize_url(base_url))
    discovered: List[str] = [start_url]
    seen = {start_url}

    if use_sitemap:
        if progress:
            progress("Sitemap kontrol ediliyor...")
        for url in fetch_sitemap_urls(start_url, limit=max_pages):
            if same_domain(start_url, url) and url not in seen:
                seen.add(url)
                discovered.append(url)
            if len(discovered) >= max_pages:
                return discovered[:max_pages]

    queue = list(discovered)
    cursor = 0
    while cursor < len(queue) and len(discovered) < max_pages:
        current = queue[cursor]
        cursor += 1
        if progress:
            progress(f"Sayfa keşfediliyor: {current}")
        page = analyze_url(current, check_link_health=False, max_links_to_check=0)
        if not page.get("ok"):
            continue
        for link in page.get("internal_links") or []:
            if link not in seen and same_domain(start_url, link):
                seen.add(link)
                discovered.append(link)
                queue.append(link)
            if len(discovered) >= max_pages:
                break
    return discovered[:max_pages]


def summarize_site(pages: List[Dict[str, Any]]) -> Dict[str, Any]:
    ok_pages = [p for p in pages if p.get("ok")]
    if not ok_pages:
        return {"score": 0, "grade": "E", "total_pages": len(pages), "ok_pages": 0, "error_pages": len(pages), "issue_counts": {}, "top_recommendations": []}

    issue_counts: Dict[str, int] = {"error": 0, "warning": 0, "success": 0, "info": 0}
    rec_counter: Dict[str, int] = {}
    duplicate_titles: Dict[str, int] = {}
    duplicate_descriptions: Dict[str, int] = {}

    for page in ok_pages:
        title = clean_text(page.get("title") or "")
        desc = clean_text(page.get("meta_description") or "")
        if title:
            duplicate_titles[title] = duplicate_titles.get(title, 0) + 1
        if desc:
            duplicate_descriptions[desc] = duplicate_descriptions.get(desc, 0) + 1
        for check in page.get("checks") or []:
            status = check.get("status", "info")
            issue_counts[status] = issue_counts.get(status, 0) + 1
            if status in {"error", "warning"}:
                rec = clean_text(check.get("recommendation") or check.get("detail") or "")
                if rec:
                    rec_counter[rec] = rec_counter.get(rec, 0) + 1

    duplicate_title_count = sum(count - 1 for count in duplicate_titles.values() if count > 1)
    duplicate_desc_count = sum(count - 1 for count in duplicate_descriptions.values() if count > 1)
    base_score = int(round(sum(int(p.get("score", 0)) for p in ok_pages) / len(ok_pages)))
    penalty = min(10, duplicate_title_count * 2 + duplicate_desc_count * 2)
    site_score = max(0, base_score - penalty)
    top_recs = sorted(rec_counter.items(), key=lambda item: item[1], reverse=True)[:12]

    return {
        "score": site_score,
        "grade": grade_for_score(site_score),
        "total_pages": len(pages),
        "ok_pages": len(ok_pages),
        "error_pages": len(pages) - len(ok_pages),
        "average_page_score": base_score,
        "duplicate_title_count": duplicate_title_count,
        "duplicate_description_count": duplicate_desc_count,
        "issue_counts": issue_counts,
        "top_recommendations": [{"recommendation": r, "count": c} for r, c in top_recs],
    }


def analyze_site(url: str, max_pages: int = 10, use_sitemap: bool = True, check_link_health: bool = False, max_links_to_check: int = 10, progress: ProgressCallback = None) -> Dict[str, Any]:
    max_pages = max(1, min(int(max_pages or 10), 100))
    if progress:
        progress("Site sayfaları hazırlanıyor...")
    urls = discover_urls(url, max_pages=max_pages, use_sitemap=use_sitemap, progress=progress)
    pages: List[Dict[str, Any]] = []
    for index, page_url in enumerate(urls, start=1):
        if progress:
            progress(f"{index}/{len(urls)} analiz ediliyor: {page_url}")
        pages.append(analyze_url(page_url, check_link_health=check_link_health, max_links_to_check=max_links_to_check))

    summary = summarize_site(pages)
    return {
        "ok": True,
        "mode": "site",
        "url": normalize_url(url),
        "final_url": pages[0].get("final_url") if pages else normalize_url(url),
        "base_site": pages[0].get("base_site") if pages else normalize_url(url),
        "score": summary["score"],
        "grade": summary["grade"],
        "summary": summary,
        "pages": pages,
        "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
    }
