import os
import sys
import tempfile
import subprocess
import random
import shutil
import shlex
from urllib.parse import urlparse
from typing import Optional

class BrowserNotFoundError(Exception):
    pass

class RenderError(Exception):
    pass

def _find_browser() -> str:
    if sys.platform == 'win32':
        import winreg
        # Try to find Chrome or Edge in the Windows Registry (Any Drive Letter/Locale)
        keys_to_check = [
            (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe"),
            (winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe"),
            (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe"),
            (winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe")
        ]
        
        for root, key_path in keys_to_check:
            try:
                with winreg.OpenKey(root, key_path) as key:
                    path, _ = winreg.QueryValueEx(key, "")
                    if os.path.exists(path):
                        return path
            except WindowsError:
                pass
                
        # Fallback to hardcoded paths if registry fails
        paths = [
            r'C:\Program Files\Google\Chrome\Application\chrome.exe',
            r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
            r'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
            r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
        ]
    elif sys.platform == 'darwin':
        paths = ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome']
    else:
        paths = ['/usr/bin/google-chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium']
        
    for p in paths:
        if os.path.exists(p):
            return p
    raise BrowserNotFoundError("No compatible browser (Chrome/Edge) found on system registry or fallback paths.")

class RenderOptions:
    """Options for rendering HTML/URL/PHP to an image"""
    def __init__(
        self,
        width: int = 1200,
        height: Optional[int] = None,
        scale: float = 2.0,
        delay: int = 0,
        transparent: bool = False,
        allow_net: bool = True,
        user_agent: Optional[str] = None,
        disable_js: bool = False,
        dark_mode: bool = False,
        full_page: bool = False,
        pdf_page_size: str = 'A4',
        pdf_landscape: bool = False,
        inject_css: Optional[str] = None,
        inject_js: Optional[str] = None,
        timeout: int = 45,
        verbose: bool = False,
        ignore_ssl_errors: bool = False
    ) -> None:
        """
        Args:
            width: Viewport width in CSS pixels (default: 1200)
            height: Viewport height in CSS pixels (default: 10000 if full_page is True)
            scale: Scale factor for output resolution equivalent to high DPI / Retina (default: 2.0)
            delay: Milliseconds to wait before capture
            transparent: Transparent background
            allow_net: Allow network requests, when set to False it bypasses CORS and allows local file access
            user_agent: Emulate specific browser device
            disable_js: Prevent execution of JavaScript
            dark_mode: Enforce `prefers-color-scheme: dark`
            full_page: Auto-crop a long screenshot
            pdf_page_size: 'A4', 'Letter' for PDFs
            pdf_landscape: Horizontal PDF orientation
            inject_css: Inject raw CSS styles (for HTML/Local files)
            inject_js: Inject raw Javascript scripts (for HTML/Local files)
            timeout: Maximum browser execution time parsing the request (default 45)
            verbose: Prints terminal debug info
            ignore_ssl_errors: Ignores incorrect HTTPS SSL certificates
        """
        self.width = width
        self.height = height if height else (10000 if full_page else 720)
        self.scale = scale
        self.delay = delay
        self.transparent = transparent
        self.allow_net = allow_net
        self.user_agent = user_agent
        self.disable_js = disable_js
        self.dark_mode = dark_mode
        self.full_page = full_page
        self.pdf_page_size = pdf_page_size
        self.pdf_landscape = pdf_landscape
        self.inject_css = inject_css
        self.inject_js = inject_js
        self.timeout = timeout
        self.verbose = verbose
        self.ignore_ssl_errors = ignore_ssl_errors

class Image:
    """A rendered image capable of converting itself into raw bytes, data or saving to files"""
    
    def __init__(self, tmp_path: str, is_pdf: bool = False, full_page: bool = False):
        self._tmp_path = tmp_path
        self._is_pdf = is_pdf
        self._width = 0
        self._height = 0
        
        # If full page is requested, try to crop empty space
        if full_page and not is_pdf:
            try:
                from PIL import Image as PILImage
                img = PILImage.open(self._tmp_path)
                bbox = img.getbbox()
                if bbox:
                    img = img.crop(bbox)
                    img.save(self._tmp_path)
                self._width, self._height = img.size
                img.close()
            except ImportError:
                pass

    @property
    def width(self) -> int:
        if self._width > 0: return self._width
        try:
            from PIL import Image as PILImage
            img = PILImage.open(self._tmp_path)
            self._width, self._height = img.size
            img.close()
        except:
            pass
        return self._width

    @property
    def height(self) -> int:
        if self._height > 0: return self._height
        return self.width and self._height

    @property
    def data(self) -> bytes:
        """Raw RGBA pixel data via Pillow"""
        try:
            from PIL import Image as PILImage
            img = PILImage.open(self._tmp_path).convert('RGBA')
            data = img.tobytes()
            img.close()
            return data
        except ImportError:
            raise RenderError("Pillow is required to extract raw pixel data.")

    def save_png(self, path: str) -> None:
        shutil.copy(self._tmp_path, path)

    def save_pdf(self, path: str) -> None:
        shutil.copy(self._tmp_path, path)

    def save_jpeg(self, path: str, quality: int = 90) -> None:
        self._save_format(path, 'JPEG', quality)

    def save_webp(self, path: str, quality: int = 90) -> None:
        self._save_format(path, 'WEBP', quality)
        
    def save_bmp(self, path: str) -> None:
        self._save_format(path, 'BMP', 100)
    
    def save_ico(self, path: str) -> None:
        try:
            from PIL import Image as PILImage
            img = PILImage.open(self._tmp_path)
            background = PILImage.new('RGBA', img.size, (255, 255, 255, 0))
            background.paste(img) 
            background.save(path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64), (128,128), (256,256)])
            background.close()
        except ImportError:
            raise RenderError("Pillow is required to save ICO.")

    def _save_format(self, path: str, fmt: str, quality: int):
        try:
            from PIL import Image as PILImage
            img = PILImage.open(self._tmp_path)
            if fmt == 'JPEG' and img.mode in ('RGBA', 'LA'):
                bg = PILImage.new('RGB', img.size, (255, 255, 255))
                bg.paste(img, mask=img.split()[3])
                img = bg
            if fmt in ['JPEG', 'WEBP']:
                img.save(path, format=fmt, quality=quality, optimize=True)
            else:
                img.save(path, format=fmt)
            img.close()
        except ImportError:
            print(f"⚠️ Pillow module not found. Saving as native PNG to {path}.")
            shutil.copy(self._tmp_path, path)

    def to_png(self) -> bytes:
        with open(self._tmp_path, 'rb') as f:
            return f.read()

    def to_pdf(self) -> bytes:
        with open(self._tmp_path, 'rb') as f:
            return f.read()

    def to_jpeg(self, quality: int = 90) -> bytes:
        return self._to_format('JPEG', quality)

    def to_webp(self, quality: int = 90) -> bytes:
        return self._to_format('WEBP', quality)

    def to_base64(self, format: str = 'PNG', quality: int = 90) -> str:
        """Returns the image encoded as a ready-to-use Base64 string for HTML injection"""
        import base64
        fmt = format.upper()
        if fmt == 'PNG':
            data = self.to_png()
        elif fmt in ['JPEG', 'JPG']:
            data = self.to_jpeg(quality)
        elif fmt == 'WEBP':
            data = self.to_webp(quality)
        else:
            data = self._to_format(fmt, quality)
        return base64.b64encode(data).decode('utf-8')

    def _to_format(self, fmt: str, quality: int) -> bytes:
        try:
            import io
            from PIL import Image as PILImage
            img = PILImage.open(self._tmp_path)
            if fmt == 'JPEG' and img.mode in ('RGBA', 'LA'):
                bg = PILImage.new('RGB', img.size, (255, 255, 255))
                bg.paste(img, mask=img.split()[3])
                img = bg
            buf = io.BytesIO()
            if fmt in ['JPEG', 'WEBP']:
                img.save(buf, format=fmt, quality=quality, optimize=True)
            else:
                img.save(buf, format=fmt)
            b = buf.getvalue()
            img.close()
            return b
        except ImportError:
            raise RenderError("Pillow is required for this conversion format in memory.")

    def __del__(self):
        try:
            if os.path.exists(self._tmp_path):
                os.remove(self._tmp_path)
        except Exception:
            pass


class Animation:
    """A rendered sequence of frames that can be saved as GIF or MP4"""
    def __init__(self, frames_paths: list, fps: int):
        self.frames = frames_paths
        self.fps = fps

    def save_gif(self, path: str) -> None:
        try:
            from PIL import Image as PILImage
            imgs = [PILImage.open(f).convert('RGBA') for f in self.frames]
            if imgs:
                imgs[0].save(path, save_all=True, append_images=imgs[1:], duration=1000//self.fps, loop=0)
                for img in imgs: img.close()
        except ImportError:
            raise RenderError("Pillow is required to generate GIF animations.")
            
    def save_mp4(self, path: str) -> None:
        import tempfile
        import shutil
        if not shutil.which('ffmpeg'):
            raise RenderError("System 'ffmpeg' is required to save as MP4. Please install ffmpeg.")
        list_file = tempfile.mktemp(suffix='.txt')
        with open(list_file, 'w') as f:
            for frame in self.frames:
                f.write(f"file '{os.path.abspath(frame).replace(chr(92), '/')}'\n")
                f.write(f"duration {1.0/self.fps}\n")
        cmd = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', list_file, '-vsync', 'vfr', '-pix_fmt', 'yuv420p', path]
        res = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        os.remove(list_file)
        if res.returncode != 0:
            raise RenderError("FFmpeg failed to generate MP4.")

    def __del__(self):
        for f in self.frames:
            if os.path.exists(f):
                try:
                    os.remove(f)
                except:
                    pass


class CoreImREngine:
    """Internal Headless Manager"""
    @classmethod
    def execute_php(cls, php_path: str) -> str:
        try:
            # Security Hardening: Ensure proper tokenization and quote the path
            safe_path = shlex.quote(php_path)
            # shlex.quote returns a string enclosed in single quotes if it contains spaces.
            # subprocess.run with check=True and list args natively escapes the arguments.
            result = subprocess.run(['php', '-f', php_path], capture_output=True, text=True, check=True)
            return result.stdout
        except FileNotFoundError:
            raise RenderError("PHP CLI not found in system PATH. Cannot render .php files natively.")
        except subprocess.CalledProcessError as e:
            raise RenderError(f"PHP Script execution failed: {e.stderr}")

    @classmethod
    def inject_code(cls, html_string: str, css: str, js: str) -> str:
        if not css and not js:
            return html_string
        head_tag = "</head>" if "</head>" in html_string.lower() else ""
        body_tag = "</body>" if "</body>" in html_string.lower() else ""

        if css:
            style_block = f"\n<style>\n/* CoreImR Injected CSS */\n{css}\n</style>\n"
            if head_tag:
                html_string = html_string.replace("</head>", style_block + "</head>", 1)
            else:
                html_string = style_block + html_string
        if js:
            script_block = f"\n<script>\n/* CoreImR Injected JS */\n{js}\n</script>\n"
            if body_tag:
                html_string = html_string.replace("</body>", script_block + "</body>", 1)
            else:
                html_string += script_block
        return html_string


def render(html: str = None, url: str = None, file_path: str = None, markdown: str = None, options: Optional[RenderOptions] = None, is_pdf: bool = False) -> Image:
    """
    Render HTML, URL, Markdown, or local file to an in-memory Image object.
    """
    if options is None:
        options = RenderOptions()

    if not html and not url and not file_path and not markdown:
        raise RenderError("You must provide either 'html', 'url', 'file_path', or 'markdown'.")

    tmp_html = None
    target_uri = url

    # Security Hardening: Validate URL scheme to prevent arbitrary protocols
    if url:
        parsed_url = urlparse(url)
        if parsed_url.scheme not in ['http', 'https', 'file']:
            raise RenderError(f"Invalid URL scheme '{parsed_url.scheme}'. Only http, https, and file are allowed.")

    def _compile_markdown(md_text: str) -> str:
        try:
            import markdown as md_parser
            raw_html = md_parser.markdown(md_text, extensions=['fenced_code', 'tables'])
            dark_css = "body { background-color: #0d1117; color: #c9d1d9; } pre, code { background: #161b22; } th, td { border-color: #30363d; } blockquote { color: #8b949e; border-color: #30363d; }" if options.dark_mode else ""
            return f"""<!DOCTYPE html><html><head><style>
            body {{ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; line-height: 1.6; padding: 40px; max-width: 900px; margin: 0 auto; color: #24292f; }}
            pre {{ background: #f6f8fa; padding: 16px; border-radius: 6px; overflow: auto; }}
            code {{ background: #f6f8fa; padding: 0.2em 0.4em; border-radius: 6px; font-family: monospace; font-size: 85%; }}
            table {{ border-collapse: collapse; width: 100%; margin-bottom: 16px; }}
            th, td {{ border: 1px solid #d0d7de; padding: 6px 13px; }}
            tr:nth-child(even) {{ background-color: rgba(175, 184, 193, 0.2); }}
            blockquote {{ border-left: 0.25em solid #dfe2e5; color: #57606a; padding: 0 1em; margin: 0 0 16px 0; }}
            img {{ max-width: 100%; }}
            {dark_css}
            </style></head><body>{raw_html}</body></html>"""
        except ImportError:
            raise RenderError("The 'markdown' package is required to parse .md files or strings natively. Run: pip install markdown")

    if markdown:
        html = _compile_markdown(markdown)

    # Parse and Inject PHP/HTML/MD
    if file_path:
        abs_file = os.path.abspath(file_path)
        if not os.path.exists(abs_file):
            raise RenderError(f"File not found: {abs_file}")
        if abs_file.lower().endswith(".php"):
            html = CoreImREngine.execute_php(abs_file)
        elif abs_file.lower().endswith(".md"):
            with open(abs_file, 'r', encoding='utf-8') as f:
                html = _compile_markdown(f.read())
        else:
            with open(abs_file, 'r', encoding='utf-8') as f:
                html = f.read()

    if html:
        html = CoreImREngine.inject_code(html, options.inject_css, options.inject_js)
        fd, tmp_html = tempfile.mkstemp(suffix=f'_coreimr_{random.randint(1000, 9999)}.html')
        with os.fdopen(fd, 'w', encoding='utf-8') as f:
            f.write(html)
        target_uri = f'file://{tmp_html}'

    browser_path = _find_browser()
    fd_out, output_path = tempfile.mkstemp(suffix='.pdf' if is_pdf else '.png')
    os.close(fd_out)

    try:
        cmd = [
            browser_path,
            '--headless',
            '--disable-gpu',
            '--hide-scrollbars',
            '--no-sandbox',
            '--disable-dev-shm-usage',
            f'--window-size={options.width},{options.height}',
        ]
        
        if is_pdf:
            cmd.append(f'--print-to-pdf={output_path}')
            cmd.append('--no-pdf-header-footer')
        else:
            cmd.append(f'--screenshot={output_path}')
            if options.transparent:
                cmd.append('--default-background-color=00000000')

        if options.delay > 0:
            cmd.append(f'--virtual-time-budget={options.delay}')
        if options.scale > 1:
            cmd.append(f'--force-device-scale-factor={options.scale}')
        if options.user_agent:
            cmd.append(f'--user-agent="{options.user_agent}"')
        if options.disable_js:
            cmd.append('--disable-javascript')
        if options.dark_mode:
            cmd.append('--force-dark-mode')
        if not options.allow_net:
            cmd.append('--disable-web-security')
            cmd.append('--allow-file-access-from-files')
        if options.ignore_ssl_errors:
            cmd.append('--ignore-certificate-errors')
            cmd.append('--ignore-ssl-errors')
            
        cmd.append(target_uri)
        
        if options.verbose:
            print("Executing headless browser command:", " ".join(cmd))
            
        result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=options.timeout)
        if result.returncode != 0:
            raise RenderError(f"Browser command failed with exit code {result.returncode}")
            
        return Image(output_path, is_pdf=is_pdf, full_page=options.full_page)
        
    except subprocess.TimeoutExpired:
        if os.path.exists(output_path): os.remove(output_path)
        raise RenderError(f"Browser rendering timed out after {options.timeout} seconds.")
    finally:
        if tmp_html:
            try:
                os.remove(tmp_html)
            except Exception:
                pass


def render_animation(html: str = None, url: str = None, file_path: str = None, options: Optional[RenderOptions] = None, duration: float = 2.0, fps: int = 15) -> Animation:
    """Renders HTML animation by capturing sequence of frames through simulated JS time seeking."""
    if options is None: options = RenderOptions()
    frames = []
    total_frames = max(1, int(duration * fps))
    for i in range(total_frames):
        time_sec = i / fps
        seek_js = f"const __T = {time_sec}; if (typeof document.getAnimations === 'function') {{ document.getAnimations().forEach(a => {{ a.pause(); a.currentTime = __T * 1000; }}); }} document.querySelectorAll('video').forEach(v => {{ v.pause(); v.currentTime = __T; }});"
        frame_js = (options.inject_js + "\n" + seek_js) if options.inject_js else seek_js
        
        frame_options = RenderOptions(
            width=options.width, height=options.height, scale=options.scale, delay=options.delay,
            transparent=options.transparent, allow_net=options.allow_net, user_agent=options.user_agent,
            disable_js=options.disable_js, dark_mode=options.dark_mode, full_page=options.full_page,
            pdf_page_size=options.pdf_page_size, pdf_landscape=options.pdf_landscape,
            inject_css=options.inject_css, inject_js=frame_js, timeout=options.timeout,
            verbose=options.verbose, ignore_ssl_errors=options.ignore_ssl_errors
        )
        
        img = render(html=html, url=url, file_path=file_path, options=frame_options)
        import tempfile
        fd, tmp_path = tempfile.mkstemp(suffix='.png')
        os.close(fd)
        img.save_png(tmp_path)
        frames.append(tmp_path)
        
    return Animation(frames, fps)


def render_to_file(
    path: str,
    html: str = None, 
    url: str = None, 
    file_path: str = None, 
    markdown: str = None,
    options: Optional[RenderOptions] = None, 
    quality: int = 90
) -> None:
    """
    Render HTML/URL/MD/PHP and save directly to a file. Format is detected from the extension.
    """
    ext = os.path.splitext(path)[1].lower()
    is_pdf = (ext == '.pdf')

    image = render(html=html, url=url, file_path=file_path, markdown=markdown, options=options, is_pdf=is_pdf)
    
    if ext == '.png':
        image.save_png(path)
    elif ext in ['.jpg', '.jpeg']:
        image.save_jpeg(path, quality=quality)
    elif ext == '.webp':
        image.save_webp(path, quality=quality)
    elif ext == '.pdf':
        image.save_pdf(path)
    elif ext == '.bmp':
        image.save_bmp(path)
    elif ext == '.ico':
        image.save_ico(path)
    else:
        # Fallback to PNG
        image.save_png(path)


# Backward compatibility for CoreImR 1.1.1 parameters directly
class CoreImR:
    def __init__(self, **kwargs):
        # Maps old 1.1.1 advanced CoreImR parameters to new RenderOptions
        scale = kwargs.pop('device_scale_factor', 2.0)
        allow_net = not kwargs.pop('disable_web_security', False)
        
        # Pull output format and quality separately
        self.output_format = kwargs.pop('output_format', 'png').lower()
        self.quality = kwargs.pop('quality', 90)
        self.pdf_landscape = kwargs.get('pdf_landscape', False)
        
        # Override old __init__ arguments matching the new signatures
        self.options = RenderOptions(scale=scale, allow_net=allow_net, **kwargs)

    def render(self, output_path: str, html: str = None, url: str = None, file_path: str = None) -> bool:
        render_to_file(path=output_path, html=html, url=url, file_path=file_path, options=self.options, quality=self.quality)
        return True

__all__ = [
    "RenderOptions",
    "Image",
    "Animation",
    "render",
    "render_to_file",
    "render_animation",
    "CoreImR"
]
