from .string_helpers import camel_to_kebab
from .string_helpers import kebab_to_camel
from .string_helpers import has_mustache
import re
import uuid
import inspect
from typing import Dict, List, Optional, Tuple
from .component_decorator import Component, load_component_from_path
import os


# ============================================================================
# HELPER: Mustache & Attributes
# ============================================================================

def convert_mustache_attrs_to_kebab_raw(html):
    """
    Converts mustache-bound attributes to kebab-case.
    e.g. someProp={value} -> some-prop={value}
    """
    def replace_attr(match):
        try:
            attr_name = match.group(1)
            equals_quote = match.group(2)
            value = match.group(3)
            end_quote = match.group(4)
        except IndexError:
            return match.group(0)

        if has_mustache(value):
            new_name = camel_to_kebab(attr_name)
            return f'{new_name}{equals_quote}{value}{end_quote}'
        return match.group(0)

    pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)(=["\'])([^"\']*?)(["\'])'
    return re.sub(pattern, replace_attr, html)


# ============================================================================
# IMPORT PARSING
# ============================================================================

def extract_imports(html: str) -> List[Dict]:
    """
    Extract all import statements from HTML.

    Supports:
        <!-- @import Test from "./path" -->
        <!-- @import Test from ./path -->  (unquoted)
        <!-- @import Test as MyTest from "./path" -->
        <!-- @import { Button, Input } from "./path" -->
        <!-- @import { Button as Btn, Input as Field } from "./path" -->
    """
    imports = []

    # Grouped imports
    grouped_pattern = r'<!--\s*@import\s*\{([^}]+)\}\s*from\s*["\']?([^"\'>\s]+)["\']?\s*-->'
    grouped_re = re.compile(grouped_pattern)
    for match in grouped_re.finditer(html):
        try:
            members = match.group(1)
            path = match.group(2)
        except IndexError:
            continue

        for member in members.split(','):
            member = member.strip()
            if not member:
                continue
            if ' as ' in member:
                parts = member.split(' as ')
                original = parts[0].strip()
                alias = parts[1].strip()
            else:
                original = member
                alias = member
            imports.append(
                {'original': original, 'alias': alias, 'path': path})

    # Single imports
    single_pattern = r'<!--\s*@import\s+(?!\{)(\w+)(?:\s+as\s+(\w+))?\s+from\s*["\']?([^"\'>\s]+)["\']?\s*-->'
    single_re = re.compile(single_pattern)
    for match in single_re.finditer(html):
        try:
            original = match.group(1)
            alias = match.group(2) or original
            path = match.group(3)
        except IndexError:
            continue
        imports.append({'original': original, 'alias': alias, 'path': path})

    return imports


def strip_imports(html: str) -> str:
    """Remove import comments from HTML"""
    html = re.sub(
        r'<!--\s*@import\s*\{[^}]+\}\s*from\s*["\']?[^"\'>\s]+["\']?\s*-->\n?', '', html
    )
    html = re.sub(
        r'<!--\s*@import\s+\w+(?:\s+as\s+\w+)?\s+from\s*["\']?[^"\'>\s]+["\']?\s*-->\n?', '', html
    )
    return html


def build_alias_map(imports: List[Dict], base_dir: str = "") -> Dict[str, Component]:
    alias_map = {}
    for imp in imports:
        comp = load_component_from_path(imp['path'], imp['original'], base_dir)
        if comp:
            alias_map[imp['alias']] = comp
    return alias_map


def process_imports(html: str, base_dir: str = "") -> tuple[str, Dict[str, Component]]:
    imports = extract_imports(html)
    alias_map = build_alias_map(imports, base_dir)
    cleaned_html = strip_imports(html)
    return cleaned_html, alias_map


# ============================================================================
# COMPONENT HELPERS
# ============================================================================

def needs_parent_scope(attrs):
    for key, value in attrs.items():
        if key.lower().startswith('on'):
            return True
        if has_mustache(value):
            return True
    return False


def wrap_scope(html, scope):
    return f'<!-- pp-scope:{scope} -->{html}<!-- /pp-scope -->'


def accepts_children(fn):
    sig = fn.__signature__ if isinstance(
        fn, Component) else inspect.signature(fn)
    params = sig.parameters
    return 'children' in params or any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values())


def _event_listener_props(props: Dict[str, str]) -> Dict[str, str]:
    return {k: v for k, v in props.items() if k.lower().startswith("on")}


def _event_attr_names(event_prop: str) -> Tuple[str, str]:
    return event_prop, camel_to_kebab(event_prop)


_RAWTEXT_ROOT_TAGS = {"textarea", "title"}


def _inject_component_scope_inside_root(html: str, component_scope: str) -> str:
    s = str(html).strip()
    if not s:
        return s

    tag_name, _, is_self_closing, end_pos = _scan_opening_tag(s, 0)
    if not tag_name:
        return wrap_scope(s, component_scope)

    if tag_name.lower() in _RAWTEXT_ROOT_TAGS or is_self_closing:
        return wrap_scope(s, component_scope)

    close_start = _scan_closing_tag(s, tag_name, end_pos)
    if close_start == -1:
        return wrap_scope(s, component_scope)

    return s[:end_pos] + f"<!-- pp-scope:{component_scope} -->" + s[end_pos:close_start] + "<!-- /pp-scope -->" + s[close_start:]


def _wrap_event_elements_with_parent_scope(html: str, event_listeners: Dict[str, str], parent_scope: str) -> str:
    if not event_listeners or not parent_scope:
        return str(html)
    s = str(html)
    wraps: List[Tuple[int, int, str]] = []

    for event_prop, handler in event_listeners.items():
        _, kebab_name = _event_attr_names(event_prop)
        camel_name = event_prop

        # Locate attribute usage
        attr_pat = re.compile(
            rf"<([a-zA-Z][\w:-]*)\b[^>]*\s(?:{re.escape(camel_name)}|{re.escape(kebab_name)})\s*=\s*(\"([^\"]*)\"|'([^']*)')[^>]*>",
            re.IGNORECASE,
        )

        for m in attr_pat.finditer(s):
            val = m.group(3) if m.group(3) is not None else (m.group(4) or "")
            if val != handler:
                continue

            tag = m.group(1)
            start = m.start()
            _, _, is_self, open_end = _scan_opening_tag(s, start)

            if is_self:
                wraps.append((start, open_end, wrap_scope(
                    s[start:open_end], parent_scope)))
                continue

            close_start = _scan_closing_tag(s, tag, open_end)
            if close_start == -1:
                continue

            close_end = s.find(">", close_start) + 1
            if close_end == 0:
                continue

            wraps.append((start, close_end, wrap_scope(
                s[start:close_end], parent_scope)))

    if not wraps:
        return s

    for start, end, repl in sorted(wraps, key=lambda x: x[0], reverse=True):
        s = s[:start] + repl + s[end:]

    return s


def _get_root_tag_name(fragment: str) -> str:
    tag, _, _, _ = _scan_opening_tag(str(fragment).strip(), 0)
    return tag.lower() if tag else ""


# ============================================================================
# ROBUST PARSING (FIXED LOGIC)
# ============================================================================

def _scan_opening_tag(html: str, start_pos: int) -> Tuple[Optional[str], str, bool, int]:
    """
    Scans a tag starting at `start_pos`.
    Returns: (tag_name, attributes_str, is_self_closing, end_pos)
    """
    length = len(html)
    if start_pos >= length or html[start_pos] != '<':
        return None, "", False, start_pos

    # 1. Extract Tag Name
    i = start_pos + 1
    while i < length and (html[i].isalnum() or html[i] in '_-:'):
        i += 1

    tag_name = html[start_pos+1:i]
    if not tag_name:
        return None, "", False, start_pos

    # 2. Scan Attributes (State Machine)
    # We only respect quotes INSIDE the tag definition.
    in_dquote = False
    in_squote = False
    attrs_start = i

    while i < length:
        char = html[i]

        if in_dquote:
            if char == '"':
                in_dquote = False
        elif in_squote:
            if char == "'":
                in_squote = False
        else:
            if char == '"':
                in_dquote = True
            elif char == "'":
                in_squote = True
            elif char == '>':
                is_self_closing = (html[i-1] == '/')
                attrs_end = i - 1 if is_self_closing else i
                attrs_str = html[attrs_start:attrs_end]
                return tag_name, attrs_str, is_self_closing, i + 1
        i += 1

    return None, "", False, start_pos


def _scan_closing_tag(html: str, tag_name: str, start_pos: int) -> int:
    """
    Finds the index of the matching `</tag_name>`.
    Handles nested tags.
    Crucially: Does NOT track quotes when scanning text content (fixing "you're" bug).
    """
    depth = 1
    length = len(html)
    i = start_pos

    target_close = f"</{tag_name}"
    target_open = f"<{tag_name}"

    while i < length:
        # Jump to next potential tag start
        lt = html.find('<', i)
        if lt == -1:
            break

        # Check what kind of tag it is
        # 1. Closing Tag?
        if html.startswith(target_close, lt):
            # Check boundary (ensure it's </Button> not </Buttons>)
            after = lt + len(target_close)
            if after < length and html[after] in '> \t\n\r':
                depth -= 1
                if depth == 0:
                    return lt
                # It was a nested closing tag, just skip it
                i = html.find('>', after) + 1
                continue

        # 2. Opening Tag? (Nesting)
        if html.startswith(target_open, lt):
            after = lt + len(target_open)
            if after < length and html[after] in '> \t\n\r/':
                # We need to check if it is self-closing
                # We use _scan_opening_tag to correctly parse attributes and find the end
                _, _, is_self, end_nested = _scan_opening_tag(html, lt)
                if not is_self:
                    depth += 1
                i = end_nested
                continue

        # 3. Comment?
        if html.startswith("<!--", lt):
            end_comment = html.find("-->", lt)
            if end_comment != -1:
                i = end_comment + 3
                continue

        # 4. Any other tag?
        # We must skip it safely so we don't get confused by '>' inside its attributes.
        # e.g. <div title=">">
        if lt + 1 < length and (html[lt+1].isalnum() or html[lt+1] == '/'):
            # Attempt to parse as a generic tag to find its end safely
            _, _, _, end_other = _scan_opening_tag(html, lt)
            # If parsing failed (malformed), just advance by 1 to avoid infinite loop
            i = end_other if end_other > lt else lt + 1
        else:
            i = lt + 1

    return -1


# ============================================================================
# RAW BLOCK PROTECTION
# ============================================================================

_RAW_TAGS_DEFAULT = ("script", "style")


def _mask_raw_blocks(html: str, tags: Tuple[str, ...] = _RAW_TAGS_DEFAULT) -> Tuple[str, List[Tuple[str, str]]]:
    blocks: List[Tuple[str, str]] = []
    for tag in tags:
        pattern = re.compile(
            rf'<{tag}\b[^>]*>[\s\S]*?</{tag}\s*>', re.IGNORECASE)

        def repl(m):
            token = f"__PP_RAW_{tag.upper()}_{uuid.uuid4().hex}__"
            blocks.append((token, m.group(0)))
            return token
        html = pattern.sub(repl, html)
    return html, blocks


def _unmask_raw_blocks(html: str, blocks: List[Tuple[str, str]]) -> str:
    for token, original in blocks:
        html = html.replace(token, original)
    return html


# ============================================================================
# MAIN TRANSFORM
# ============================================================================

def transform_components(
    html: str,
    parent_scope: str = "app",
    base_dir: str = "",
    alias_map: Optional[Dict[str, Component]] = None,
    _depth: int = 0
) -> str:
    if _depth > 50:
        return html

    # 0. Mask script/style blocks
    html, raw_blocks = _mask_raw_blocks(html)

    # 1. Process imports
    html, local_alias_map = process_imports(html, base_dir)
    merged_map = {**(alias_map or {}), **local_alias_map}

    if not merged_map:
        return _unmask_raw_blocks(html, raw_blocks)

    # 2. Attributes
    html = convert_mustache_attrs_to_kebab_raw(html)

    # 3. Main Loop
    max_iterations = 100
    iteration = 0

    while iteration < max_iterations:
        iteration += 1

        # Optimization: Find candidates via regex, parse via lexer
        candidate_pattern = re.compile(r'<([A-Z][a-zA-Z0-9]*)')
        best_match = None

        for m in candidate_pattern.finditer(html):
            name = m.group(1)
            if name in merged_map:
                best_match = (m.start(), name)
                break

        if not best_match:
            break

        found_start, found_tag_name = best_match
        component_fn = merged_map[found_tag_name]

        tag_name, attrs_str, is_self_closing, open_end = _scan_opening_tag(
            html, found_start)
        if not tag_name:
            break

        children_html = ""
        full_end = open_end

        if not is_self_closing:
            close_start = _scan_closing_tag(html, tag_name, open_end)
            if close_start != -1:
                children_html = html[open_end:close_start]
                full_end = html.find('>', close_start) + 1
            else:
                print(
                    f"[Caspian] Warning: Unclosed component <{tag_name}> found.")
                break

        # Props Parsing
        props: Dict[str, str] = {}
        attr_pattern = r'([a-zA-Z_][\w\-]*)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|(\S+))'

        for attr_match in re.finditer(attr_pattern, attrs_str):
            try:
                attr_name = attr_match.group(1)
                val = attr_match.group(2) if attr_match.group(2) is not None else \
                    attr_match.group(3) if attr_match.group(3) is not None else \
                    attr_match.group(4) or ""
                props[kebab_to_camel(attr_name)] = val
            except IndexError:
                continue

        # Boolean props
        attrs_clean = re.sub(r'"[^"]*"|\'[^\']*\'', '""', attrs_str)
        bool_attr_pattern = r'\s([a-zA-Z_][\w\-]*)(?=\s|/?>|$)(?!=)'
        for bool_match in re.finditer(bool_attr_pattern, attrs_clean):
            try:
                camel = kebab_to_camel(bool_match.group(1))
                if camel not in props:
                    props[camel] = ""
            except IndexError:
                continue

        unique_id = f"{tag_name.lower()}_{uuid.uuid4().hex[:8]}"

        if children_html and accepts_children(component_fn):
            props['children'] = wrap_scope(children_html, parent_scope)

        try:
            component_html = component_fn(**props)
        except Exception as e:
            print(f"[Caspian] Error rendering <{tag_name}>: {e}")
            break

        # Recursion
        if hasattr(component_fn, 'source_path') and component_fn.source_path:
            component_dir = os.path.dirname(component_fn.source_path)
            component_html = transform_components(
                component_html,
                parent_scope=unique_id,
                base_dir=component_dir,
                _depth=_depth + 1
            )

        # Inject ID
        component_html = re.sub(
            r'^(\s*<[a-zA-Z][a-zA-Z0-9]*)',
            rf'\1 pp-component="{unique_id}"',
            str(component_html).strip(),
            count=1
        )

        # Scoping
        root_tag = _get_root_tag_name(component_html)
        needs_parent = needs_parent_scope(props)
        event_listeners = _event_listener_props(props)

        if root_tag in _RAWTEXT_ROOT_TAGS:
            if needs_parent and parent_scope:
                component_html = wrap_scope(component_html, parent_scope)
            else:
                component_html = wrap_scope(component_html, unique_id)
        else:
            if needs_parent and parent_scope:
                component_html = _inject_component_scope_inside_root(
                    component_html, unique_id)
                if event_listeners:
                    component_html = _wrap_event_elements_with_parent_scope(
                        component_html,
                        event_listeners=event_listeners,
                        parent_scope=parent_scope,
                    )
                component_html = wrap_scope(component_html, parent_scope)
            else:
                component_html = wrap_scope(component_html, unique_id)

        html = html[:found_start] + component_html + html[full_end:]

    return _unmask_raw_blocks(html, raw_blocks)
