import re
from typing import List, Dict, Any, Tuple, Optional, Literal
from .colors import cprint, Colors
import time
from .encryption import Encryption
import json
import os
from ..type.errors import CreateSessionError

def _collect_matches(text: str, patterns: Dict[str, List[str]], priority: Dict[str, int]) -> List[Dict[str, Any]]:
    matches = []
    for style, pats in patterns.items():
        for pat in pats:
            for m in re.finditer(pat, text, flags=re.DOTALL | re.MULTILINE):
                start, end = m.start(), m.end()
                groups = m.groups()
                content = ""
                extra = None

                if style == "Link" and len(groups) >= 2:
                    content = groups[0]
                    extra = groups[1]
                elif style == "HTMLLink" and len(groups) >= 2:
                    extra = groups[0]
                    content = groups[1]
                elif style == "MentionHTML" and len(groups) >= 2:
                    extra = groups[0]
                    content = groups[1]
                else:
                    if len(groups) >= 1:
                        content = groups[0]
                    else:
                        content = m.group(0)

                if style in ("Pre", "PreHTML"):
                    content = content.strip("\n")

                matches.append({
                    "start": start,
                    "end": end,
                    "style": style,
                    "content": content,
                    "full_match": m.group(0),
                    "extra": extra,
                    "priority": priority.get(style, 50)
                })
    return matches

def _allow_match(chosen: List[Dict[str, Any]], candidate: Dict[str, Any]) -> bool:
    s, e = candidate["start"], candidate["end"]
    for c in chosen:
        os, oe = c["start"], c["end"]
        if (s >= os and e <= oe) or (os >= s and oe <= e):
            continue
        if not (e <= os or s >= oe):
            return False
    return True

def _pick_matches_allowing_nested(matches: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    matches_sorted = sorted(matches, key=lambda m: (m['priority'], m['start']))
    chosen = []
    for m in matches_sorted:
        if _allow_match(chosen, m):
            chosen.append(m)
    chosen.sort(key=lambda m: m['start'])
    return chosen

class TextParser:
    @staticmethod
    def checkMarkdown(text: str) -> Tuple[List[Dict[str, Any]], str]:
        """
        Parse Markdown and return flat metadata list + plain text.
        - Nested styles are preserved as separate metadata entries (flat list).
        - Partial/cross overlaps are rejected.
        """
        if not text:
            return [], ""

        patterns = {
            "Pre": [r"```(?:[^\n]*\n)?([\s\S]*?)```"],
            "Link": [r"\[([^\]]+?)\]\(([^)]+?)\)"],  # [text](url)
            "MentionMD": [r"@@([^@]+?)@@"],
            "CodeInline": [r"`([^`]+?)`"],
            "Spoiler": [r"\|\|([^|]+?)\|\|"],
            "Bold": [r"\*\*([^\*]+?)\*\*", r"__(?<!_)([^_]+?)__(?!_)"],
            "Strike": [r"~~([^~]+?)~~"],
            "Underline": [r"__(?<!_)([^_]+?)__(?!_)"],
            "Italic": [r"(?<!\*)\*([^*\n]+?)\*(?!\*)", r"(?<!_)_([^_\n]+?)_(?!_)"],
            "Blockquote": [r"(^> .+(?:\n> .+)*)"]
        }

        priority = {
            "Pre": 0,
            "Link": 1,
            "MentionMD": 1,
            "CodeInline": 2,
            "Spoiler": 3,
            "Bold": 4,
            "Strike": 5,
            "Underline": 6,
            "Italic": 7,
            "Blockquote": 8
        }

        all_matches = _collect_matches(text, patterns, priority)
        chosen = _pick_matches_allowing_nested(all_matches)

        out_parts: List[str] = []
        metadata: List[Dict[str, Any]] = []
        last = 0

        for m in chosen:
            if last < m['start']:
                out_parts.append(text[last:m['start']])
            current_index = sum(len(p) for p in out_parts)
            out_parts.append(m['content'])
            length = len(m['content'])
            st = m['style']

            if st == "Link":
                metadata.append({
                    "type": "Link",
                    "from_index": current_index,
                    "length": length,
                    "link_url": m['extra']
                })
            elif st == "MentionMD":
                metadata.append({
                    "type": "MentionText",
                    "from_index": current_index,
                    "length": length,
                    "mention_text_object_guid": m['content'],
                    "mention_text_user_id": m['content'],
                    "mention_text_object_type": "user"
                })
            elif st == "CodeInline":
                metadata.append({
                    "type": "Mono",
                    "from_index": current_index,
                    "length": length
                })
            elif st == "Pre":
                metadata.append({
                    "type": "Pre",
                    "from_index": current_index,
                    "length": length
                })
            elif st == "Blockquote":
                lines = re.sub(r"^> ?", "", m['content'], flags=re.MULTILINE)
                metadata.append({
                    "type": "Blockquote",
                    "from_index": current_index,
                    "length": length
                })
            else:
                map_types = {
                    "Bold": "Bold",
                    "Italic": "Italic",
                    "Underline": "Underline",
                    "Strike": "Strike",
                    "Spoiler": "Spoiler"
                }
                meta_type = map_types.get(st, st)
                metadata.append({
                    "type": meta_type,
                    "from_index": current_index,
                    "length": length
                })

            last = m['end']

        if last < len(text):
            out_parts.append(text[last:])

        real_text_final = "".join(out_parts)
        return metadata, real_text_final

    @staticmethod
    def checkHTML(text: str) -> Tuple[List[Dict[str, Any]], str]:
        """
        Parse simple HTML-like tags and return flat metadata + plain text.
        Supports <b>, <strong>, <i>, <em>, <code>, <s>, <del>, <u>, <pre>, <span class="tg-spoiler">,
        <a href="...">text</a>, <mention objectId="...">text</mention>, and rubika:// links.
        """
        if text is None:
            return [], ""

        patterns = {
            "PreHTML": [r"<pre>([\s\S]*?)</pre>"],
            "HTMLLink": [r'<a\s+href="([^"]+?)">([^<]+?)</a>'],
            "MentionHTML": [r'<mention\s+objectId="([^"]+?)">([^<]+?)</mention>'],
            "CodeInlineHTML": [r"<code>([^<]+?)</code>"],
            "SpoilerHTML": [r'<span\s+class="tg-spoiler">([^<]+?)</span>'],
            "BoldHTML": [r"<b>([^<]+?)</b>", r"<strong>([^<]+?)</strong>"],
            "ItalicHTML": [r"<i>([^<]+?)</i>", r"<em>([^<]+?)</em>"],
            "StrikeHTML": [r"<s>([^<]+?)</s>", r"<del>([^<]+?)</del>"],
            "UnderlineHTML": [r"<u>([^<]+?)</u>"],
        }

        priority = {
            "PreHTML": 0,
            "HTMLLink": 1,
            "MentionHTML": 1,
            "CodeInlineHTML": 2,
            "SpoilerHTML": 3,
            "BoldHTML": 4,
            "ItalicHTML": 5,
            "StrikeHTML": 6,
            "UnderlineHTML": 7
        }

        all_matches = _collect_matches(text, patterns, priority)
        chosen = _pick_matches_allowing_nested(all_matches)

        out_parts: List[str] = []
        metadata: List[Dict[str, Any]] = []
        last = 0

        for m in chosen:
            if last < m['start']:
                out_parts.append(text[last:m['start']])
            current_index = sum(len(p) for p in out_parts)
            out_parts.append(m['content'])
            length = len(m['content'])
            st = m['style']

            if st == "HTMLLink":
                url = m['extra']
                if url and url.startswith("rubika://"):
                    uid = url.replace("rubika://", "")
                    metadata.append({
                        "type": "MentionText",
                        "from_index": current_index,
                        "length": length,
                        "mention_text_object_guid": uid,
                        "mention_text_user_id": uid,
                        "mention_text_object_type": "user"
                    })
                else:
                    metadata.append({
                        "type": "Link",
                        "from_index": current_index,
                        "length": length,
                        "link_url": url
                    })
            elif st == "MentionHTML":
                object_id = m['extra'] or m['content']
                metadata.append({
                    "type": "MentionText",
                    "from_index": current_index,
                    "length": length,
                    "mention_text_object_guid": object_id,
                    "mention_text_user_id": object_id,
                    "mention_text_object_type": "group"
                })
            elif st == "CodeInlineHTML":
                metadata.append({
                    "type": "Mono",
                    "from_index": current_index,
                    "length": length
                })
            elif st == "PreHTML":
                metadata.append({
                    "type": "Pre",
                    "from_index": current_index,
                    "length": length
                })
            elif st == "SpoilerHTML":
                metadata.append({
                    "type": "Spoiler",
                    "from_index": current_index,
                    "length": length
                })
            else:
                map_html = {
                    "BoldHTML": "Bold",
                    "ItalicHTML": "Italic",
                    "UnderlineHTML": "Underline",
                    "StrikeHTML": "Strike"
                }
                meta_type = map_html.get(st, st)
                metadata.append({
                    "type": meta_type,
                    "from_index": current_index,
                    "length": length
                })

            last = m['end']

        if last < len(text):
            out_parts.append(text[last:])

        real_text_final = "".join(out_parts)
        return metadata, real_text_final

class utils:
    @staticmethod
    def format_file(type_file: Optional[str] = None) -> Optional[str]:
        if not type_file:
            return None
        for type_,pass_ in {"File":"", "Image":".png", "Voice":".mp3", "Music":".mp3", "Gif":".mp4" , "Video":".mp4"}.items():
            if type_ == type_file:
                name_file = type_+pass_
                return name_file
        return None
    
    @staticmethod
    def print_time(text: str, time_sleep: float = 0.07, color: str = Colors.WHITE) -> None:
        k = ""
        for text in text:
            k += text
            print(f"{color}{k}{Colors.RESET}", end="\r")
            time.sleep(time_sleep)
        cprint("",Colors.WHITE)
    
    @staticmethod
    def get_input(text_output: str) -> str:
        text = None
        while text is None or len(text) != 64:
            cprint("Write the valid ! Your text invalid.",Colors.RED)
            text = input(text_output)
        return text
    
    @staticmethod
    def calculate_upload_timeout(file_size_bytes: int, upload_speed_bps: int = 300_000) -> int:
        SAFETY_FACTOR = 1.5
        timeout_seconds = (file_size_bytes / upload_speed_bps) * SAFETY_FACTOR
        return max(int(timeout_seconds), 30)

    # Session
    
    @staticmethod
    def session_dict(name_session: str, token: str, user_agent: Optional[str] = None, time_out: Optional[float] = 30.0, display_welcome: bool = False, view_logs: Optional[bool] = False, save_logs: Optional[bool] = False) -> dict:
        text_json_fast_rub_session = {
            "name_session": name_session,
            "token": token,
            "user_agent": user_agent,
            "time_out": time_out,
            "display_welcome": display_welcome,
            "setting_logs":{
                "view":view_logs,
                "save":save_logs
            }
        }
        return text_json_fast_rub_session
    
    @staticmethod
    def save_dict(data: dict, name_file: str):
        json_fast_rub_session = json.dumps(data,indent=4,ensure_ascii=False)
        json_fast_rub_session = Encryption().en(str(json_fast_rub_session))
        utils.save(json_fast_rub_session, name_file)

    @staticmethod
    def save(data: str, name_file: str):
        with open(name_file, "w", encoding="utf-8") as file:
            file.write(data)

    @staticmethod
    def create_session(name_session: str, token: Optional[str] = None, user_agent: Optional[str] = None, time_out: Optional[float] = 30.0, display_welcome: bool = False, view_logs: Optional[bool] = False, save_logs: Optional[bool] = False) -> bool:
        try:
            if token is None:
                token = utils.get_input("Write The Token » ")
            session = utils.session_dict(name_session,token,user_agent,time_out,display_welcome,view_logs,save_logs)
            utils.save_dict(session, f"{name_session}.faru")
            return True
        except:
            return False
    
    @staticmethod
    def open_session(name_session: str, token: Optional[str] = None, user_agent: Optional[str] = None, time_out: Optional[float] = 30.0, display_welcome: bool = False, view_logs: Optional[bool] = False, save_logs: Optional[bool] = False) -> dict:
        """بازکردن سشن

        Args:
            name_session (str): اسم سشن
            token (Optional[str], optional): توکن. Defaults to None.
            user_agent (Optional[str], optional): اطلاعات مرورگر درخواست کننده. Defaults to None.
            time_out (Optional[int], optional): زمان خروج. Defaults to 30.
            display_welcome (bool, optional): خوش آمد گویی. Defaults to False.
            view_logs (Optional[bool], optional): نمایش لاگ ها. Defaults to False.
            save_logs (Optional[bool], optional): ذخیره لاگ ها. Defaults to False.

        Raises:
            CreateSessionError: خطا برای ساخت سشن

        Returns:
            dict: اطلاعات سشن
        """
        path_session = name_session + ".faru"
        if os.path.isfile(path_session):
            with open(path_session, "r", encoding="utf-8") as file:
                encrypted_string = file.read().strip()
            try:
                decrypted = Encryption().de(encrypted_string)
                session = json.loads(decrypted)
                return session
            except:
                cprint("Error for getting last data session !", Colors.RED)
                creating = False
                while not creating:
                    creating = utils.create_session(name_session,token,user_agent,time_out,display_welcome,view_logs,save_logs)
                    if not creating:
                        raise CreateSessionError("Can Not Create The Session !")
                return utils.open_session(name_session,token,user_agent,time_out,display_welcome,view_logs,save_logs)
        else:
            creating = False
            while not creating:
                creating = utils.create_session(name_session,token,user_agent,time_out,display_welcome,view_logs,save_logs)
                if not creating:
                    raise CreateSessionError("Can Not Create The Session !")
            return utils.open_session(name_session,token,user_agent,time_out,display_welcome,view_logs,save_logs)
    
