from typing import Any, Generator, TextIO
from bs4 import BeautifulSoup, NavigableString, Comment, Tag as BSTag
import os
import threading
import re
from ..key_handler import KeyHandler
from ..form_elements import TextElement
from ..subdialogs import Applet
from .editor import EditDialog
from ..framework import beep
from .. import punctuator
from .. import unspeakables
import webbrowser
import tempfile
test_html_string = """
<!DOCTYPE html>
<html>
<head>
    <title>Test Page</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>This is a test HTML page.</p>
</body>
</html>
"""


def collapse_whitespace(navigable_string: NavigableString) -> str:
    """Mimics browser behavior by collapsing consecutive whitespace.

    Collapses all consecutive whitespace in a NavigableString into a single
    space, unless it is inside a <pre> or <textarea> element.

    Args:
        navigable_string: BeautifulSoup NavigableString to process.

    Returns:
        Collapsed text string.
    """
    if navigable_string.parent.name in ['pre', 'textarea']:
        # Do not collapse whitespace in <pre> or <textarea>
        return navigable_string
    # Collapse whitespace for other cases
    collapsed_text = ' '.join(navigable_string.split())
    return collapsed_text
# test string containing pre and textarea 
test2_html = """
<div>
    This    is a      test.  
    <pre>    Keep   this  spacing! </pre>
    <textarea>    Maintain   this spacing  too.  </textarea>
</div>
"""


def apply_formatting(soup: BeautifulSoup, start_position: tuple[Any, int], end_position: tuple[Any, int], tag_string: str) -> None:
    """
    Wrap a specified range in the soup with the provided HTML tag.
    Args:
        soup (BeautifulSoup): The BeautifulSoup object.
        start_position (tuple): (FormattedTextElement object, offset within line).
        end_position (tuple): (FormattedTextElement object, offset within line).
        tag_string (str): HTML tag string to use for wrapping (e.g., "<i>", "<a href='url'>").
    """
    # Parse the tag string to create a new BeautifulSoup tag
    tag_soup = BeautifulSoup(tag_string, "html.parser")
    wrapper_tag = tag_soup.contents[0]  # Extract the actual tag object
    # Extract start and end elements and offsets
    start_line, start_offset = start_position
    end_line, end_offset = end_position
    # Traverse soup to find the start and end elements
    in_range = False  # Flag indicating if we're within the start-end range
    elements_to_wrap = []  # Elements within the specified range
    for element in soup.descendants:
        if isinstance(element, NavigableString):
            if element == start_line.start.navigable_string:
                # Split start element if necessary
                if start_offset > 0:
                    before_text = element[:start_offset]
                    after_text = element[start_offset:]
                    element.replace_with(NavigableString(before_text), element)
                    element = element.next_sibling
                    element.replace_with(after_text)
                in_range = True  # Start wrapping from this point
            if in_range:
                elements_to_wrap.append(element)
            if element == end_line.end.navigable_string:
                # Split end element if necessary
                if end_offset < len(element):
                    before_text = element[:end_offset]
                    after_text = element[end_offset:]
                    element.replace_with(before_text, NavigableString(after_text))
                break  # Stop after reaching the end element
    # Wrap all collected elements in the desired tag
    for el in elements_to_wrap:
        wrapper_tag.append(el.extract())  # Remove from soup and add to wrapper
    # Insert the wrapped content back into soup at the first element's position
    elements_to_wrap[0].insert_before(wrapper_tag)


def apply_basic_formatting(soup: BeautifulSoup, start_position: tuple[Any, int], end_position: tuple[Any, int], format_type: str = "italic") -> None:
    """
    Apply basic formatting to a specified range in the soup.
    Args:
        soup (BeautifulSoup): The BeautifulSoup object.
        start_position (tuple): (FormattedTextElement object, offset within line).
        end_position (tuple): (FormattedTextElement object, offset within line).
        format_type (str): Type of formatting ("italic" or "heading1").
    """
    # Define the HTML tag based on the format type
    if format_type == "italic":
        tag_name = "i"
    elif format_type == "heading1":
        tag_name = "h1"
    else:
        raise ValueError("Unsupported format type. Use 'italic' or 'heading1'.")
    # Extract start and end elements and offsets
    start_line, start_offset = start_position
    end_line, end_offset = end_position
    # Traverse through soup to find the start and end elements
    in_range = False  # Tracks if we are within the start-end range
    elements_to_wrap = []  # Holds the elements that need wrapping
    for element in soup.descendants:
        if isinstance(element, NavigableString):
            if element == start_line.start.navigable_string:
                # Split start element if necessary
                if start_offset > 0:
                    before_text = element[:start_offset]
                    element.replace_with(NavigableString(before_text), element)
                    element = element.next_sibling
                in_range = True  # Start wrapping from this point
            if in_range:
                elements_to_wrap.append(element)
            if element == end_line.end.navigable_string:
                # Split end element if necessary
                if end_offset < len(element):
                    after_text = element[end_offset:]
                    element.replace_with(element[:end_offset], NavigableString(after_text))
                break  # Stop after reaching the end element
    # Wrap all collected elements in the desired HTML tag
    wrapper = soup.new_tag(tag_name)
    for el in elements_to_wrap:
        wrapper.append(el.extract())  # Remove from soup and add to wrapper
    # Insert the wrapped content back into soup at the first element's position
    elements_to_wrap[0].insert_before(wrapper)


class SoupPosition:
    """Represents a position within a NavigableString in BeautifulSoup.

    Attributes:
        navigable_string: The NavigableString object.
        offset: Character offset within the string.
    """

    def __init__(self, navigable_string: NavigableString, offset: int) -> None:
        self.navigable_string = navigable_string
        self.offset = offset


class UrlHandler:
    """Handles different URL protocols (http, https, ftp, tel, mailto).

    Routes URLs to protocol-specific handlers based on their protocol prefix.
    """

    def handle(self, url: str) -> None:
        """Route URL to appropriate protocol handler.

        Args:
            url: URL string to handle.
        """
        # Extract protocol from the URL
        protocol = self.extract_protocol(url)
        # Construct the handler method name and call it, defaulting to default_handler
        method_name = f"{protocol}_handler"
        getattr(self, method_name, self.default_handler)(url)

    def extract_protocol(self, url: str) -> str | None:
        # Match the protocol at the beginning of the URL
        match = re.match(r"^([a-zA-Z][a-zA-Z0-9+-.]*):", url)
        return match.group(1) if match else None

    # Define default handler
    def default_handler(self, url: str) -> None:
        print(f"unsupported protocol for URL: {url}")

    def launch_browser(self, url: str) -> None:
        threading.Thread(target=webbrowser.open, args=(url,)).start()

    # Define protocol-specific handlers
    def handle_http(self, url: str) -> None:
        self.launch_browser(url)

    def handle_https(self, url: str) -> None:
        self.launch_browser(url)

    def handle_ftp(self, url: str) -> None:
        self.launch_browser(url)

    def handle_tel(self, url: str) -> None:
        print(f"Implement tel url handler that uses windows phone app to  call: {url}")

    def handle_mailto(self, url: str) -> None:
        print(f"Implement email link handler that repurposes this dialog with new message to : {url}")


class Tag:
    """Represents an HTML tag with position and attributes."""

    def __init__(self, name: str, pos: int, length: int, attributes: dict[str, Any], description: str = "", description_extension: str = "") -> None:
        self.name = name
        self.pos = pos
        self.length = length
        self.attributes = attributes
        self.description = description
        self.description_extension = description_extension
        self.index: int = 0
        self.unlabelled_graphic: bool = False


class TaggedLine:
    """Represents a line of text with associated HTML tags."""

    def __init__(self, line_start: int, line: str, tags: list[Tag]) -> None:
        self.line_start = line_start
        self.line = line
        self.tags = tags


def split_and_wrap(full_text: str) -> list[tuple[int, str]]:
    """Split text into lines and return with start positions."""
    lines = full_text.split('\n')
    result = []
    pos = 0
    for line in lines:
        result.append((pos, line))
        pos += len(line) + 1  # +1 for newline
    return result


class SoupBowl:
    """Parses HTML and extracts text with tag metadata.

    Renders HTML content into plain text while tracking positions and
    attributes of significant tags (links, headings, images, etc.).
    """

    def __init__(self, html_content: str) -> None:
        self.handlers: dict[str, Any] = {
            "a": self.link_handler, "article": self.block_handler, "aside": self.block_handler,
            "blockquote": self.block_handler, "br": self.block_handler, "div": self.block_handler,
            "footer": self.block_handler, "header": self.block_handler,
            "h1": self.heading_handler, "h2": self.heading_handler, "h3": self.heading_handler,
            "h4": self.heading_handler, "h5": self.heading_handler, "h6": self.heading_handler,
            "img": self.img_handler, "nav": self.block_handler, "p": self.block_handler,
            "pre": self.block_handler, "section": self.block_handler
        }
        self.current_pos: int = 0
        self.current_tag_index: int = 0
        self.full_text: str = ''
        self.tags: list[Tag] = []
        self.soup = BeautifulSoup(html_content, 'html.parser')
        self._render(self.soup.body if self.soup.body else self.soup)

    def _render(self, element: Any) -> None:
        """ 
        Recursively traverse the element tree and build the full text and the interesting tags  attributes, skipping elements of no interest to winston, such as aria-hidden elements, script and style. 
        """
        if isinstance(element, NavigableString):
            if not isinstance(element,Comment): #ignore comments
                # Handle plain text by cleaning chars which tts cannot speak then appending it
                text=unspeakables.substitute(element,unspeakables.unspeakables)
                self.full_text+=text
                self.current_pos += len(text)
            return
        elif element.has_attr('aria-hidden') and element['aria-hidden'].lower() == 'true':
            return   # Ignore this element and its children
        elif  element.name in ['script','style']: 
            return   # Ignore this element and its children 
        else:
            handler=self.handlers.get(element.name,self.default_handler)
            tag=handler(element)
            if tag: 
                self.current_tag_index+=1
                tag.index=self.current_tag_index 
                self.tags.append(tag)
        # If the element has children, recursively process them
        for child in element.children:
            self._render(child)

    def get_tagged_lines(self) -> list[TaggedLine]:
        tagged_lines = self._apply_tags(self.full_text, self.tags)
        return tagged_lines

    def _apply_tags(self, full_text: str, tags: list[Tag]) -> list[TaggedLine]:
        start_and_text_tuples = split_and_wrap(full_text)
        tagged_lines = []
        for line_start, line in start_and_text_tuples:
            tagged_lines.append(TaggedLine(line_start, line, tags))
        return tagged_lines

    def link_handler(self, soup_link: BSTag) -> Tag:
        text = unspeakables.substitute(soup_link.get_text(), unspeakables.unspeakables)
        url = soup_link.get('href')
        t = Tag('link', self.current_pos, len(text), {'url': url}, description_extension=text)
        children = [c for c in soup_link.children]
        if (len(children) == 1) and (children[0].name == 'img') and not children[0].get('alt'):
            t.unlabelled_graphic = True
        return t

    def default_handler(self, soup_element: BSTag) -> None:
        #print(f'default handler for {soup_element.name}')
        pass

    def heading_handler(self, soup_heading: BSTag) -> Tag:
        text = unspeakables.substitute(soup_heading.get_text(), unspeakables.unspeakables)
        description = f"heading level {soup_heading.name[-1]}"
        return Tag(soup_heading.name, self.current_pos, len(text), attributes={}, description=description, description_extension=text)

    def block_handler(self, soup_element: BSTag) -> None:
        if not self.full_text.endswith('\n'):  # Avoid duplicate line breaks
            self.full_text += '\n'
            self.current_pos += 1

    def img_handler(self, soup_img: BSTag) -> Tag:
        description = soup_img.get('alt', 'unlabelled')+' graphic '
        # insert a placeholder into the text parts and update the current position so that multiple images on a single line do not occupy the same slot in the eventual TaggedElement()  
        placeholder = '\u200c' #'\u200c' #This is  non-breaking space not spoken in punc.ini. do not use ' graphic ' as this leads to saying of simply space when entering the tag
        self.full_text+=placeholder
        pos = self.current_pos
        self.current_pos += len(placeholder)
        t = Tag(soup_img.name, pos, len(placeholder), {}, description)
        t.unlabelled_graphic = False if soup_img.get('alt') else True
        return t


class FormattedTextElement(TextElement):
    """TextElement that references BeautifulSoup NavigableString positions.

    Used for editing HTML content while preserving the underlying soup structure.

    Attributes:
        start: SoupPosition marking the start of this element.
        end: SoupPosition marking the end of this element.
    """

    def __init__(self, *args: Any, start: SoupPosition | None = None, end: SoupPosition | None = None, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.start = start
        self.end = end

    @property
    def value(self) -> str:
        """
        returns the text from the NavigableString referenced by this FormattedTextElement object
        collapses all whitespace in the same way as a visual browser
        Needs enhancing to return text from multiple or partial NavigableString objects to cope with wrapping and with consolidation of multiple NavigableStrings
        consider whether soup.smooth() has a role to play in consolidating NavigableStrings
        """
        if hasattr(self,'start') and self.start:
            return collapse_whitespace(self.start.navigable_string)[self.start.offset:]
        else:
            return 'invalid element not referencing a navigable string'

    @value.setter
    def value(self, val: str) -> None:
        before_text = self.start.navigable_string[:self.caret]
        after_text = self.start.navigable_string[self.caret:]
        plain_text=True
        #this is broken as val is the entire string to be assigned to the element.value not just the char which has been typed, so seeing if can make work for plain text as is but will need rethink if input is html 
        if False: #len(val)>1: #may be an html fragment 
            # Check if multi_character content is plain text or HTML
            parsed_content = BeautifulSoup(val, "html.parser")
            if not (len(parsed_content.contents) == 1 and isinstance(parsed_content.contents[0], NavigableString)):
                plain_text=False
        if  plain_text:  #update the existing NavigableString directly
            #new_content = NavigableString(before_text + val + after_text)
            new_content = NavigableString(val)            
        else:
            new_content = [NavigableString(before_text) ,parsed_content,NavigableString(after_text)]
            #following 4 lines create a span with the before,content and after elements, rather than just inserting them as is. Do not know if this is better but try it if issues with the simpler structure
            new_content = BeautifulSoup(before_text, "html.parser").new_tag('span')
            new_content.append(NavigableString(before_text))
            new_content.append(parsed_content)
            new_content.append(NavigableString(after_text))
        self.start.navigable_string.replace_with(new_content)
        self.start.navigable_string = new_content

    def zzz_delete(self, start: SoupPosition, end: SoupPosition) -> None:
        deleting = False
        for element in self.soup.descendants:
            if element == start.navigable_string:
                element.replace_with(NavigableString(element[:start.offset]) + NavigableString(element[end.offset + 1:]))
                deleting = True
            elif deleting and isinstance(element, NavigableString):
                if element == end.navigable_string:
                    element.replace_with(NavigableString(element[end.offset + 1:]))
                    break
                else:
                    element.extract()


class SoupDialog(EditDialog):
    """HTML editor dialog extending EditDialog with BeautifulSoup integration.

    Provides HTML editing with soup-aware element management and browser preview.

    Attributes:
        soup: BeautifulSoup object representing the HTML document.
        max_line_length: Maximum line length for wrapping.
    """

    def element_attributes(self) -> set[str]:
        return super().element_attributes().union({'format_string'})

    def __init__(self, *args: Any, element_type: type[FormattedTextElement] = FormattedTextElement, **kwargs: Any) -> None:
        self.soup: BeautifulSoup | None = None
        self.max_line_length: int = 160
        super().__init__(*args, element_type=element_type, **kwargs)

    def contents_from_file(self, open_file: TextIO | None = None, element_type: type[FormattedTextElement] = FormattedTextElement) -> Generator[FormattedTextElement, None, None]:
        """
        called in context of an open file 
        if there is no open_file then create a minimal body for a new webpage
        assign element_type to winston_element_type to distinguish from html_element 
        """
        winston_element_type = element_type
        if open_file:
            html_string=open_file.read()
        else:
            html_string='<html><body> </body></html>'
        #print(html_string)
        self.soup=BeautifulSoup(html_string,'html.parser')
        for html_element in self.soup.descendants:
            if isinstance(html_element, NavigableString):
                    start = SoupPosition(html_element, 0)
                    end = SoupPosition(html_element, -1)
                    e = winston_element_type(self, 'blank line', '', start=start, end=end)
                    yield e

    def file_from_contents(self, open_file: TextIO) -> None:
        """Override EditDialog.file_from_contents to write soup HTML.

        Args:
            open_file: File object to write to.
        """
        open_file.write(str(self.soup))

    @classmethod
    def new_template(cls) -> str:
        return 'new webpage.html'

    @KeyHandler.hotkey("Alt+U")
    def command_view_in_browser(self, modified_name: str) -> None:
        with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as temp_file:
            #replace test_html_string with str(self.soup)
            temp_file.write(str(self.soup).encode("utf-8"))
            temp_file_path = temp_file.name
        # Open the temporary file in the default browser
        #webbrowser.open(f"file://{os.path.abspath(temp_file_path)}")
        #threading.Thread(target=webbrowser.open, args=(f"file://{os.path.abspath(temp_file_path)}",)).start()
        os.startfile(temp_file_path)


class SouperDialog(Applet, SoupDialog):
    """Souper applet combining Applet and SoupDialog for HTML editing."""
    pass        