#!/usr/bin/env python3
"""
Smart HTML to Markdown converter.

Detects content structure (forums, blogs, Q&A, e-commerce) and generates
clean markdown that preserves semantic meaning.
"""

from bs4 import BeautifulSoup, Tag
from typing import Optional, List, Dict, Tuple
from urllib.parse import urljoin
import re


class SmartMarkdownConverter:
    """Convert HTML to markdown with semantic structure preservation."""

    def __init__(self, base_url: str = ''):
        self.base_url = base_url

    def convert(self, html: str, hint: str = None) -> str:
        """Convert HTML to markdown, auto-detecting content type.

        Args:
            html: Raw HTML content
            hint: Optional content type hint ('forum', 'blog', 'qa', 'ecommerce') to skip detection
        """
        # lxml is 2-5x faster than html.parser, fall back if not installed
        try:
            soup = BeautifulSoup(html, 'lxml')
        except Exception:
            soup = BeautifulSoup(html, 'html.parser')

        # Remove noise
        self._remove_noise(soup)

        # Fast path: if hint provided, skip detection
        if hint == 'forum':
            posts = soup.select('.post_container, .topic-post, .post-stream > article')
            if posts:
                return self._render_forum(self._extract_forum_posts(soup, posts))

        # Detect content type and extract accordingly
        content_type, content = self._detect_and_extract(soup)

        if content_type == 'forum':
            return self._render_forum(content)
        elif content_type == 'blog':
            return self._render_blog(content)
        elif content_type == 'qa':
            return self._render_qa(content)
        elif content_type == 'ecommerce':
            return self._render_ecommerce(content)
        else:
            # Fallback: clean conversion of main content
            return self._render_generic(soup)

    def _remove_noise(self, soup: BeautifulSoup):
        """Remove navigation, scripts, styles, and other noise."""
        # Remove script, style, and other non-content elements
        for tag in soup(['script', 'style', 'noscript', 'iframe', 'svg']):
            tag.decompose()

        # Remove common navigation/chrome elements - single combined selector
        noise_selector = ', '.join([
            'nav', 'header', 'footer',
            '.sidebar', '.nav', '.navigation', '.menu', '.breadcrumb',
            '.header', '.footer', '.logo', '.site-logo',
            '#header', '#footer', '#nav', '#sidebar',
            '.d-header', '.d-footer',
            '.pagination', '.pager',
            '[role="banner"]', '[role="navigation"]', '[role="contentinfo"]',
            '.cookie-banner', '.modal', '.popup', '.overlay',
            '.ad', '.ads', '.advertisement', '.sponsored',
            '.social-share', '.share-buttons',
            # Table of contents (various CMS patterns)
            '.contents.topic', '#contents', '.toc-wrapper',
        ])
        for tag in soup.select(noise_selector):
            tag.decompose()

    def _detect_and_extract(self, soup: BeautifulSoup) -> Tuple[str, any]:
        """Detect content type and extract structured content."""

        # Forum detection (Discourse, phpBB, vBulletin, Reddit, etc.)
        forum_indicators = [
            '.post_container',  # Discourse archived (has avatar_container + post)
            '.topic-post',  # Discourse live
            '.post-stream > article',  # Discourse alt
            '.message',  # XenForo
            '.postcontainer',  # vBulletin
            'article.boxed',  # Generic
            '.thing.comment',  # Reddit old
            'shreddit-comment',  # Reddit new
            '.Comment',  # Reddit redesign
            '.remarkbox-comment',  # Remarkbox
            '.comment',  # Generic comments
        ]
        for post_sel in forum_indicators:
            posts = soup.select(post_sel)
            if len(posts) >= 2:  # At least 2 posts to be a forum thread
                return 'forum', self._extract_forum_posts(soup, posts)

        # Q&A detection (Stack Exchange, etc.)
        qa_indicators = [
            ('.question', '.answer'),
            ('#question', '.answer'),
            ('.question-page', '.answercell'),
        ]
        for q_sel, a_sel in qa_indicators:
            questions = soup.select(q_sel)
            answers = soup.select(a_sel)
            if questions or answers:
                return 'qa', self._extract_qa(soup, questions, answers)

        # Blog detection
        blog_indicators = [
            'article', '.post', '.entry', '.blog-post',
            '.hentry', '[itemtype*="BlogPosting"]',
        ]
        for selector in blog_indicators:
            articles = soup.select(selector)
            if articles and len(articles) <= 10:  # Not a listing page
                return 'blog', self._extract_blog(soup, articles)

        # E-commerce detection
        ecom_indicators = [
            '.product', '.product-detail', '[itemtype*="Product"]',
            '.product-info', '.pdp-main',
        ]
        for selector in ecom_indicators:
            products = soup.select(selector)
            if products:
                return 'ecommerce', self._extract_ecommerce(soup, products)

        return 'generic', soup

    def _extract_forum_posts(self, soup: BeautifulSoup, posts: List[Tag]) -> List[Dict]:
        """Extract forum posts with avatar, username, content, timestamp."""
        extracted = []

        for post in posts:
            post_data = {
                'avatar': None,
                'username': None,
                'timestamp': None,
                'content': None,
                'quotes': [],
            }

            # Avatar - look for common patterns (may be sibling or child)
            avatar = (
                post.select_one('.avatar_container img.avatar') or  # Discourse archived
                post.select_one('.avatar_container img') or
                post.select_one('.avatar-container img') or
                post.select_one('img.avatar') or
                post.select_one('.avatar img') or
                post.select_one('.avatar-flair img') or
                post.select_one('.user-avatar img') or
                post.select_one('.postprofile img') or
                post.select_one('.author img') or
                post.select_one('img[alt*="avatar"]') or  # Reddit
                post.select_one('.flair img') or  # Reddit flair
                post.select_one('.remarkbox-avatar img')  # Remarkbox
            )
            if avatar:
                src = avatar.get('src', '')
                # Skip placeholder avatars
                if src and '{size}' not in src:
                    post_data['avatar'] = self._resolve_url(src)

            # Username - Discourse archived uses .user_name, live uses .username
            username_el = (
                post.select_one('.user_name') or  # Discourse archived
                post.select_one('.username') or
                post.select_one('.author') or
                post.select_one('.creator a') or
                post.select_one('.user-card-name') or
                post.select_one('a[data-user-card]') or
                post.select_one('.names .name') or
                post.select_one('.postprofile dt') or
                post.select_one('strong.username') or
                post.select_one('a[href*="/user/"]') or  # Reddit
                post.select_one('a[href*="/u/"]') or  # Reddit shorthand
                post.select_one('.remarkbox-author')  # Remarkbox
            )
            if username_el:
                post_data['username'] = username_el.get_text(strip=True)

            # Timestamp
            time_el = (
                post.select_one('time') or
                post.select_one('.post-date') or
                post.select_one('.timestamp') or
                post.select_one('.date') or
                post.select_one('.relative-date')
            )
            if time_el:
                post_data['timestamp'] = time_el.get('title') or time_el.get_text(strip=True)

            # Content - the main post body
            content_el = (
                post.select_one('.post_content') or  # Discourse archived
                post.select_one('.cooked') or  # Discourse live
                post.select_one('.post-content') or
                post.select_one('.message-body') or
                post.select_one('.postcontent') or
                post.select_one('.post_body') or
                post.select_one('.entry-content') or
                post.select_one('.content') or
                post.select_one('.post') or  # Fallback to inner .post div
                post.select_one('.md') or  # Reddit markdown content
                post.select_one('.usertext-body') or  # Reddit old
                post.select_one('.remarkbox-content')  # Remarkbox
            )
            if content_el:
                post_data['content'] = self._element_to_markdown(content_el)
            else:
                # Fallback: use whole post but try to exclude metadata
                clone = BeautifulSoup(str(post), 'html.parser')
                for sel in ['.avatar', '.avatar_container', '.user_name', '.username', '.author', '.date', '.post-actions', '.post-menu']:
                    for el in clone.select(sel):
                        el.decompose()
                post_data['content'] = self._element_to_markdown(clone)

            if post_data['content']:
                extracted.append(post_data)

        return extracted

    def _extract_qa(self, soup: BeautifulSoup, questions: List[Tag], answers: List[Tag]) -> Dict:
        """Extract Q&A content with votes, user info."""
        qa_data = {'question': None, 'answers': []}

        if questions:
            q = questions[0]
            qa_data['question'] = {
                'title': self._get_text(q, '.question-title, h1'),
                'votes': self._get_text(q, '.vote-count, .js-vote-count'),
                'content': self._element_to_markdown(q.select_one('.post-text, .s-prose, .question-body')),
                'author': self._get_text(q, '.user-info .user-details a, .author'),
            }

        for a in answers:
            answer_data = {
                'votes': self._get_text(a, '.vote-count, .js-vote-count'),
                'content': self._element_to_markdown(a.select_one('.post-text, .s-prose, .answer-body')),
                'author': self._get_text(a, '.user-info .user-details a, .author'),
                'accepted': bool(a.select_one('.accepted-answer, .is-accepted')),
            }
            if answer_data['content']:
                qa_data['answers'].append(answer_data)

        return qa_data

    def _extract_blog(self, soup: BeautifulSoup, articles: List[Tag]) -> List[Dict]:
        """Extract blog articles with metadata."""
        extracted = []

        for article in articles:
            article_data = {
                'title': self._get_text(article, 'h1, h2, .entry-title, .post-title'),
                'author': self._get_text(article, '.author, .byline, [rel="author"]'),
                'date': self._get_text(article, 'time, .date, .published, .post-date'),
                'content': None,
            }

            content_el = (
                article.select_one('.entry-content') or
                article.select_one('.post-content') or
                article.select_one('.article-body') or
                article.select_one('.article_text') or
                article.select_one('.article-text') or
                article.select_one('.content') or
                article
            )
            article_data['content'] = self._element_to_markdown(content_el)

            if article_data['content']:
                extracted.append(article_data)

        return extracted

    def _extract_ecommerce(self, soup: BeautifulSoup, products: List[Tag]) -> List[Dict]:
        """Extract product information."""
        extracted = []

        for product in products:
            product_data = {
                'name': self._get_text(product, '.product-name, .product-title, h1, h2'),
                'price': self._get_text(product, '.price, .product-price, [itemprop="price"]'),
                'description': self._element_to_markdown(
                    product.select_one('.description, .product-description, [itemprop="description"]')
                ),
                'image': None,
                'rating': self._get_text(product, '.rating, .stars, [itemprop="ratingValue"]'),
            }

            img = product.select_one('.product-image img, .gallery img, [itemprop="image"]')
            if img:
                product_data['image'] = self._resolve_url(img.get('src', ''))

            if product_data['name']:
                extracted.append(product_data)

        return extracted

    def _element_to_markdown(self, el: Optional[Tag]) -> str:
        """Convert a single element to markdown."""
        if not el:
            return ''

        lines = []
        self._walk_element(el, lines)
        return '\n'.join(lines).strip()

    def _walk_element(self, el: Tag, lines: List[str], depth: int = 0):
        """Recursively walk element and build markdown lines."""
        if isinstance(el, str):
            text = el.strip()
            if text:
                lines.append(text)
            return

        if not isinstance(el, Tag):
            return

        tag = el.name.lower() if el.name else ''

        # Block elements
        if tag in ('p', 'div'):
            # Check if contains block-level elements (including nested divs)
            block_tags = {'div', 'p', 'pre', 'ul', 'ol', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'section', 'article'}
            has_blocks = any(
                isinstance(c, Tag) and c.name and c.name.lower() in block_tags
                for c in el.children
            )
            if has_blocks:
                # Recursively process children to preserve block structure
                for child in el.children:
                    self._walk_element(child, lines, depth)
            else:
                content = self._inline_content(el)
                if content:
                    lines.append(content)
                    lines.append('')

        elif tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
            level = int(tag[1])
            content = self._inline_content(el)
            if content:
                lines.append(f"{'#' * level} {content}")
                lines.append('')

        elif tag == 'blockquote':
            quote_lines = []
            for child in el.children:
                self._walk_element(child, quote_lines, depth + 1)
            for line in quote_lines:
                if line:
                    lines.append(f"> {line}")
                else:
                    lines.append('>')
            lines.append('')

        elif tag in ('ul', 'ol'):
            for i, li in enumerate(el.find_all('li', recursive=False)):
                prefix = f"{i+1}." if tag == 'ol' else "-"
                content = self._inline_content(li)
                lines.append(f"{prefix} {content}")
            lines.append('')

        elif tag == 'pre':
            code = el.get_text() or ''
            # Try to detect language from class
            lang = ''
            code_el = el.select_one('code')
            if code_el:
                classes = code_el.get('class', [])
                for cls in classes:
                    if cls.startswith('language-') or cls.startswith('lang-'):
                        lang = cls.split('-', 1)[1]
                        break
            lines.append(f"```{lang}")
            lines.append(code.strip())
            lines.append("```")
            lines.append('')

        elif tag == 'code' and el.parent and el.parent.name != 'pre':
            # Inline code handled in _inline_content
            pass

        elif tag == 'hr':
            lines.append('---')
            lines.append('')

        elif tag == 'br':
            lines.append('')

        elif tag == 'img':
            src = self._resolve_url(el.get('src', ''))
            alt = el.get('alt', '')
            if src:
                lines.append(f"![{alt}]({src})")
                lines.append('')

        elif tag == 'a':
            # Links are handled inline
            pass

        elif tag == 'table':
            lines.extend(self._table_to_markdown(el))
            lines.append('')

        else:
            # Generic container - recurse
            for child in el.children:
                self._walk_element(child, lines, depth)

    def _inline_content(self, el: Tag) -> str:
        """Extract inline content from an element, handling formatting."""
        parts = []

        for child in el.children:
            if isinstance(child, str):
                text = child.strip()
                if text:
                    # Collapse whitespace
                    text = re.sub(r'\s+', ' ', text)
                    parts.append(text)
            elif isinstance(child, Tag):
                tag = child.name.lower() if child.name else ''

                if tag in ('strong', 'b'):
                    inner = self._inline_content(child)
                    if inner:
                        parts.append(f"**{inner}**")

                elif tag in ('em', 'i'):
                    inner = self._inline_content(child)
                    if inner:
                        parts.append(f"*{inner}*")

                elif tag == 'code':
                    code = child.get_text()
                    if code:
                        parts.append(f"`{code}`")

                elif tag == 'a':
                    href = self._resolve_url(child.get('href', ''))
                    text = self._inline_content(child) or href
                    if href:
                        parts.append(f"[{text}]({href})")
                    else:
                        parts.append(text)

                elif tag == 'img':
                    src = self._resolve_url(child.get('src', ''))
                    alt = child.get('alt', '')
                    if src:
                        # Emoji images (alt like :smile:) stay inline
                        if alt.startswith(':') and alt.endswith(':'):
                            parts.append(f"![{alt}]({src})")
                        else:
                            # Regular images are block-level
                            parts.append(f"\n\n![{alt}]({src})\n\n")

                elif tag == 'br':
                    parts.append('\n')

                elif tag in ('span', 'small', 'mark'):
                    # Pass through
                    inner = self._inline_content(child)
                    if inner:
                        parts.append(inner)

                else:
                    # Unknown inline - just get text
                    inner = self._inline_content(child)
                    if inner:
                        parts.append(inner)

        return ' '.join(parts).strip()

    def _table_to_markdown(self, table: Tag) -> List[str]:
        """Convert HTML table to markdown."""
        lines = []
        rows = table.find_all('tr')
        if not rows:
            return lines

        # Extract headers
        header_row = rows[0]
        headers = [self._inline_content(th) for th in header_row.find_all(['th', 'td'])]
        if headers:
            lines.append('| ' + ' | '.join(headers) + ' |')
            lines.append('| ' + ' | '.join(['---'] * len(headers)) + ' |')

        # Extract body rows
        for row in rows[1:]:
            cells = [self._inline_content(td) for td in row.find_all(['td', 'th'])]
            if cells:
                # Pad cells if needed
                while len(cells) < len(headers):
                    cells.append('')
                lines.append('| ' + ' | '.join(cells) + ' |')

        return lines

    def _get_text(self, el: Tag, selectors: str) -> str:
        """Get text from first matching selector."""
        for selector in selectors.split(','):
            found = el.select_one(selector.strip())
            if found:
                return found.get_text(strip=True)
        return ''

    def _resolve_url(self, url: str) -> str:
        """Resolve relative URL to absolute, keeping same-page anchors as fragments."""
        if not url or url.startswith('data:'):
            return url

        # Keep anchor-only links as-is
        if url.startswith('#'):
            return url

        if self.base_url:
            # Check if this is a same-page link with anchor
            if '#' in url:
                from miniuri import Uri
                resolved = urljoin(self.base_url, url)
                base_uri = Uri(self.base_url)
                resolved_uri = Uri(resolved)
                # Same host and path = same page, keep just the fragment
                if (base_uri.hostname == resolved_uri.hostname and
                    base_uri.path.rstrip('/') == resolved_uri.path.rstrip('/')):
                    return '#' + resolved.split('#', 1)[1]
                return resolved

            if not url.startswith(('http://', 'https://', '//')):
                return urljoin(self.base_url, url)

        return url

    # Renderers for different content types

    def _render_forum(self, posts: List[Dict]) -> str:
        """Render forum posts to markdown."""
        lines = []

        for i, post in enumerate(posts):
            if i > 0:
                lines.append('')
                lines.append('---')
                lines.append('')

            # Post header with avatar and username
            # Always include avatar (placeholder if none) to maintain grid layout
            if post['avatar']:
                avatar_md = f"![avatar]({post['avatar']})"
            else:
                # Use # as placeholder - JS will detect and replace with initial
                avatar_md = "![avatar](#)"

            header_parts = [avatar_md]
            if post['username']:
                header_parts.append(f"**{post['username']}**")
            if post['timestamp']:
                header_parts.append(f"*{post['timestamp']}*")

            lines.append(' '.join(header_parts))
            lines.append('')

            # Post content
            if post['content']:
                lines.append(post['content'])

        return '\n'.join(lines)

    def _render_qa(self, qa: Dict) -> str:
        """Render Q&A content to markdown."""
        lines = []

        if qa['question']:
            q = qa['question']
            if q['title']:
                lines.append(f"# {q['title']}")
                lines.append('')
            if q['votes']:
                lines.append(f"**{q['votes']} votes**")
            if q['author']:
                lines.append(f"*Asked by {q['author']}*")
            lines.append('')
            if q['content']:
                lines.append(q['content'])
            lines.append('')

        if qa['answers']:
            lines.append('---')
            lines.append('')
            lines.append(f"## {len(qa['answers'])} Answers")
            lines.append('')

            for answer in qa['answers']:
                if answer['accepted']:
                    lines.append('### ✓ Accepted Answer')
                else:
                    lines.append('### Answer')
                if answer['votes']:
                    lines.append(f"**{answer['votes']} votes**")
                if answer['author']:
                    lines.append(f"*By {answer['author']}*")
                lines.append('')
                if answer['content']:
                    lines.append(answer['content'])
                lines.append('')
                lines.append('---')
                lines.append('')

        return '\n'.join(lines)

    def _render_blog(self, articles: List[Dict]) -> str:
        """Render blog articles to markdown."""
        lines = []

        for article in articles:
            content = article['content'] or ''

            # Only add title if it's not already at the start of content
            if article['title']:
                title_md = f"# {article['title']}"
                if not content.startswith(title_md) and not content.startswith(f"# [{article['title']}"):
                    lines.append(title_md)
                    lines.append('')

            meta = []
            if article['author']:
                meta.append(f"By {article['author']}")
            if article['date']:
                meta.append(article['date'])
            if meta:
                lines.append(f"*{' | '.join(meta)}*")
                lines.append('')

            if content:
                lines.append(content)

        return '\n'.join(lines)

    def _render_ecommerce(self, products: List[Dict]) -> str:
        """Render product info to markdown."""
        lines = []

        for product in products:
            if product['name']:
                lines.append(f"# {product['name']}")
                lines.append('')

            if product['image']:
                lines.append(f"![{product['name']}]({product['image']})")
                lines.append('')

            if product['price']:
                lines.append(f"**Price:** {product['price']}")
                lines.append('')

            if product['rating']:
                lines.append(f"**Rating:** {product['rating']}")
                lines.append('')

            if product['description']:
                lines.append(product['description'])

        return '\n'.join(lines)

    def _render_generic(self, soup: BeautifulSoup) -> str:
        """Fallback generic rendering."""
        # Find main content area
        main = (
            soup.select_one('main') or
            soup.select_one('article') or
            soup.select_one('.content') or
            soup.select_one('#content') or
            soup.select_one('.main') or
            soup.body or
            soup
        )

        return self._element_to_markdown(main)


def html_to_markdown(html: str, base_url: str = '', hint: str = None) -> str:
    """Convert HTML to markdown with smart structure detection.

    Args:
        html: Raw HTML content
        base_url: Base URL for resolving relative links
        hint: Content type hint ('forum', 'blog', 'qa', 'ecommerce') to skip auto-detection

    Returns:
        Clean markdown with preserved semantic structure
    """
    converter = SmartMarkdownConverter(base_url)
    return converter.convert(html, hint=hint)


if __name__ == '__main__':
    import sys

    if len(sys.argv) < 2:
        print("Usage: python html2md.py <file.html> [base_url]")
        sys.exit(1)

    with open(sys.argv[1]) as f:
        html = f.read()

    base_url = sys.argv[2] if len(sys.argv) > 2 else ''
    print(html_to_markdown(html, base_url))
