"""Predefined tools for LLMs"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_tools.ipynb.

# %% auto 0
__all__ = ['view', 'create', 'insert', 'str_replace', 'scrape_url', 'read_url']

# %% ../nbs/03_tools.ipynb 2
from bs4 import BeautifulSoup
from cloudscraper import create_scraper
from html2text import HTML2Text
from pathlib import Path
from urllib.parse import urljoin, urlparse
import re
from textwrap import dedent

# %% ../nbs/03_tools.ipynb 9
def view(path:str,  # The path to the file or directory to view
         view_range:tuple[int,int]=None, # Optional array of two integers specifying the start and end line numbers to view. Line numbers are 1-indexed, and -1 for the end line means read to the end of the file. This parameter only applies when viewing files, not directories.
         nums:bool=False # Optionally prefix all lines of the file with a line number
        ) -> str:
    'Examine the contents of a file or list the contents of a directory. It can read the entire file or a specific range of lines. With or without line numbers.'
    try:
        p = Path(path).expanduser().resolve()
        if not p.exists(): return f'Error: File not found: {p}'
        if p.is_dir():
            res = [str(f) for f in p.glob('**/*') 
                   if not any(part.startswith('.') for part in f.relative_to(p).parts)]
            return f'Directory contents of {p}:\n' + '\n'.join(res)
        
        lines = p.read_text().splitlines()
        s,e = 1,len(lines)
        if view_range:
            s,e = view_range
            if not (1 <= s <= len(lines)): return f'Error: Invalid start line {s}'
            if e != -1 and not (s <= e <= len(lines)): return f'Error: Invalid end line {e}'
            lines = lines[s-1:None if e==-1 else e]
            
        return '\n'.join([f'{i+s-1:6d} │ {l}' for i,l in enumerate(lines,1)] if nums else lines)
    except Exception as e: return f'Error viewing file: {str(e)}'
     

# %% ../nbs/03_tools.ipynb 11
def create(path: str, # The path where the new file should be created
           file_text: str, # The text content to write to the new file
           overwrite:bool=False # Allows overwriting an existing file
          ) -> str:
    'Creates a new file with the given text content at the specified path'
    try:
        p = Path(path)
        if p.exists():
            if not overwrite: return f'Error: File already exists: {p}'
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(file_text)
        return f'Created file {p} containing:\n{file_text}'
    except Exception as e: return f'Error creating file: {str(e)}'     

# %% ../nbs/03_tools.ipynb 13
def insert(path: str,  # The path to the file to modify
           insert_line: int, # The line number after which to insert the text (0 for beginning of file)
           new_str: str # The text to insert
          ) -> str: 
    'Insert text at a specific line number in a file.'
    try:
        p = Path(path)
        if not p.exists(): return f'Error: File not found: {p}'
            
        content = p.read_text().splitlines()
        if not (0 <= insert_line <= len(content)): return f'Error: Invalid line number {insert_line}'
            
        content.insert(insert_line, new_str)
        new_content = '\n'.join(content)
        p.write_text(new_content)
        return f'Inserted text at line {insert_line} in {p}.\nNew contents:\n{new_content}'
    except Exception as e: return f'Error inserting text: {str(e)}'

# %% ../nbs/03_tools.ipynb 15
def str_replace(path: str, # The path to the file to modify
                old_str: str, # The text to replace (must match exactly, including whitespace and indentation)
                new_str: str # The new text to insert in place of the old text
               ) -> str:
    'Replace a specific string in a file with a new string. This is used for making precise edits.'
    try:
        p = Path(path)
        if not p.exists(): return f'Error: File not found: {p}'
            
        content = p.read_text()
        count = content.count(old_str)
        
        if count == 0: return 'Error: Text not found in file'
        if count > 1: return f'Error: Multiple matches found ({count})'
            
        new_content = content.replace(old_str, new_str, 1)
        p.write_text(new_content)
        return f'Replaced text in {p}.\nNew contents:\n{new_content}'
    except Exception as e: return f'Error replacing text: {str(e)}'

# %% ../nbs/03_tools.ipynb 23
def _absolutify_imgs(md, base_url):
    """This function rewrites Markdown image links so their URLs become absolute, using a base URL.
    - md: a Markdown string
    - base_url: the base URL used to resolve relative path
    """
    def fix(m):
        alt, img_url = m.group(1), m.group(2)
        if not img_url.startswith('http'): img_url = urljoin(base_url, img_url)
        return f'![{alt}]({img_url})'
    return re.sub(r'!\[(.*?)\]\((.*?)\)', fix, md)


# %% ../nbs/03_tools.ipynb 24
def _convert_math(soup, mode):
    """This function walks an HTML/XML document, finds MathML math elements, and replaces them with TeX/LaTeX markup suitable for text-based math renderers (like Markdown, MathJax, or KaTeX).
    - soup: a BeautifulSoup object representing parsed HTML/XML
    - mode: controls the inline math delimiter style
    Two inline math styles are supported:
    - mode='dollar': Inline math $x$ Display math $$x$$
    - other	Inline math \\(...\\) Display math $$x$$
    """
    for math in soup.find_all('math'):
        annot = math.find('annotation', {'encoding': 'application/x-tex'})
        if not annot:
            continue
        tex, display = annot.text.strip(), math.get('display') == 'block'
        if mode == 'dollar':
            wrap = f'$${tex}$$' if display else f'${tex}$'
        else:
            wrap = f'$${tex}$$' if display else f'\\({tex}\\)'
        math.replace_with(wrap)

# %% ../nbs/03_tools.ipynb 25
def scrape_url(url):
    "Get the html content of a web page using the cloudscraper library to bypass Cloudflare's anti-bot page."
    return create_scraper().get(url)

def read_url(url: str, as_md: bool = True, extract_section: bool = True, selector: str = None, math_mode: str = None):
    """This functions extracts a web page information for LLM ingestion
    1. Downloads a web page
    2. Parses HTML
    3. Optionally extracts a specific section (fragment or CSS selector)
    4. Converts MathML → LaTeX
    5. Optionally converts HTML → Markdown
    6. Convert code sections to fenced markdown blocks
    7. Makes image URLs absolute
    8. Returns the processed text
    """
    o = scrape_url(url)
    res, ctype = o.text, o.headers.get('content-type').split(';')[0]
    soup = BeautifulSoup(res, 'lxml')

    if selector:
        res = '\n\n'.join(str(s) for s in soup.select(selector))
    elif extract_section:
        parsed = urlparse(url)
        if parsed.fragment:
            section = soup.find(id=parsed.fragment)
            if section:
                elements = [section]
                current = section.next_sibling
                while current:
                    if hasattr(current, 'name') and current.name == section.name: break
                    elements.append(current)
                    current = current.next_sibling
                res = ''.join(str(el) for el in elements)
            else:
                res = ''
    else:
        res = str(soup)

    if math_mode:
        res_soup = BeautifulSoup(res, 'lxml')
        _convert_math(res_soup, math_mode)
        res = str(res_soup)

    if as_md and ctype == 'text/html':
        h = HTML2Text()
        h.body_width = 0
        # Handle code blocks
        h.mark_code = True
        res = h.handle(res)
        def _f(m): return f'```\n{dedent(m.group(1))}\n```'
        res = re.sub(r'\[code]\s*\n(.*?)\n\[/code]', _f, res or '', flags=re.DOTALL).strip()
        # Handle image urls
        res = _absolutify_imgs(res, urljoin(url, s['href'] if (s := soup.find('base')) else ''))
        # Handle math blocks
        if math_mode == 'safe':
            res = res.replace('\\\\(', '\\(').replace('\\\\)', '\\)')

    return res

# %% ../nbs/03_tools.ipynb 43
# import ...
