#!/usr/bin/env python3
"""Phase 3: Static source code analysis for Activities — extract layout bindings, click handlers,
menus, dialogs, fragments, dynamic views, adapter item layouts, and dialog custom layouts
from Kotlin/Java source files."""

import json
import os
import re
import sys


def analyze_activity_source(source_file):
    """Analyze an Activity source file and extract UI-related information."""
    try:
        with open(source_file, "r", encoding="utf-8", errors="ignore") as f:
            content = f.read()
    except Exception as e:
        return {"error": str(e)}

    result = {
        "source_file": source_file,
        "layout_file": None,
        "binding_class": None,
        "actions": [],
        "menus": [],
        "dialogs": [],
        "fragments": [],
        "dynamic_elements": [],
    }

    # 1. Extract layout binding
    # setContentView(R.layout.xxx)
    m = re.search(r'setContentView\s*\(\s*R\.layout\.(\w+)\s*\)', content)
    if m:
        result["layout_file"] = m.group(1)

    # ViewBinding: XxxBinding.inflate(...)
    m = re.search(r'(\w+Binding)\.inflate\s*\(', content)
    if not m:
        # Also try: by viewBinding(XxxBinding::inflate)
        m = re.search(r'viewBinding\s*\(\s*(\w+Binding)\s*::\s*inflate\s*\)', content)
    if not m:
        # Also try: import ...databinding.XxxBinding
        m = re.search(r'import\s+[\w.]+\.databinding\.(\w+Binding)', content)
    if m:
        result["binding_class"] = m.group(1)
        # Derive layout name from binding class: ActivityMainBinding -> activity_main
        binding_name = m.group(1)
        if binding_name.endswith("Binding"):
            # Convert CamelCase to snake_case
            layout_name = re.sub(r'(?<!^)(?=[A-Z])', '_', binding_name[:-7]).lower()
            if not result["layout_file"]:
                result["layout_file"] = layout_name

    # 2. Extract click handlers
    # binding.viewName.setOnClickListener { ... }
    for m in re.finditer(
        r'binding\.(\w+)\s*\.\s*setOnClickListener\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}',
        content, re.DOTALL
    ):
        view_name = m.group(1)
        body = m.group(2)
        view_id = _camel_to_snake(view_name)
        action = _analyze_click_body(body, view_id)
        result["actions"].append(action)

    # setOnClickListener with lambda reference
    for m in re.finditer(
        r'(\w+)\s*\.\s*setOnClickListener\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}',
        content, re.DOTALL
    ):
        var_name = m.group(1)
        if var_name == "binding":
            continue  # Already handled above
        body = m.group(2)
        action = _analyze_click_body(body, var_name)
        if action not in result["actions"]:
            result["actions"].append(action)

    # setOnLongClickListener
    for m in re.finditer(
        r'binding\.(\w+)\s*\.\s*setOnLongClickListener\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)',
        content, re.DOTALL
    ):
        view_name = m.group(1)
        body = m.group(2)
        view_id = _camel_to_snake(view_name)
        action = _analyze_click_body(body, view_id, event="onLongClick")
        result["actions"].append(action)

    # 3. Extract menus
    # Patterns to detect:
    #   menuInflater.inflate(R.menu.xxx, menu)
    #   inflateMenu(R.menu.xxx)
    #   binding.xxxMenu.getToolbar().inflateMenu(R.menu.xxx)
    #   toolbar.inflateMenu(R.menu.xxx)
    #   menu = R.menu.xxx (variable assignment in setupOptionsMenu)
    #   Bare R.menu.xxx references inside menu setup methods
    seen_menus = set()
    menu_patterns = [
        r'inflate\s*\(\s*R\.menu\.(\w+)',           # menuInflater.inflate(R.menu.xxx)
        r'inflateMenu\s*\(\s*R\.menu\.(\w+)',       # toolbar.inflateMenu(R.menu.xxx)
        r'R\.menu\.(\w+)',                          # bare R.menu.xxx (catch-all)
    ]
    for pat in menu_patterns:
        for m in re.finditer(pat, content):
            menu_file = m.group(1)
            if menu_file not in seen_menus:
                seen_menus.add(menu_file)
                result["menus"].append({"menu_file": menu_file})

    # onOptionsItemSelected / onMenuItemClick handlers
    menu_handler_pattern = re.compile(
        r'R\.id\.(\w+)\s*->\s*\{?([^}]*(?:\{[^}]*\}[^}]*)*)\}?',
        re.DOTALL
    )
    # Find onOptionsItemSelected blocks
    item_selected_match = re.search(
        r'onOptionsItemSelected\s*\([^)]*\)\s*[:{]\s*(.*?)(?=\n\s*(?:override\s+fun|fun\s|private\s|protected\s|\}))',
        content, re.DOTALL
    )
    if item_selected_match:
        block = item_selected_match.group(1)
        for mm in menu_handler_pattern.finditer(block):
            item_id = mm.group(1)
            body = mm.group(2)
            action = _analyze_click_body(body, item_id, event="onMenuItemClick")
            result["actions"].append(action)

    # 4. Extract dialog usage
    # Custom dialog classes instantiated
    for m in re.finditer(r'(\w+Dialog)\s*\(', content):
        dialog_class = m.group(1)
        if dialog_class in ("AlertDialog", "ProgressDialog"):
            continue
        result["dialogs"].append({
            "dialog_class": dialog_class,
            "type": "custom",
        })

    # AlertDialog.Builder
    for m in re.finditer(r'AlertDialog\.Builder\s*\(', content):
        result["dialogs"].append({"dialog_class": "AlertDialog", "type": "alert"})

    # MaterialAlertDialogBuilder
    for m in re.finditer(r'MaterialAlertDialogBuilder\s*\(', content):
        result["dialogs"].append({"dialog_class": "MaterialAlertDialog", "type": "material_alert"})

    # 5. Extract fragments
    # FragmentTransaction / supportFragmentManager
    for m in re.finditer(r'\.replace\s*\(\s*R\.id\.(\w+)\s*,\s*(\w+)\s*[\(,]', content):
        container_id = m.group(1)
        fragment_class = m.group(2)
        result["fragments"].append({
            "fragment_class": fragment_class,
            "container_id": container_id,
        })

    # ViewPager adapter references
    for m in re.finditer(r'(\w+Adapter)\s*\(', content):
        adapter_class = m.group(1)
        if "Pager" in adapter_class or "Fragment" in adapter_class:
            result["fragments"].append({
                "fragment_class": adapter_class,
                "container_id": None,
                "type": "pager_adapter",
            })

    # 6. Extract dynamic views
    for m in re.finditer(r'addView\s*\(\s*(\w+)', content):
        result["dynamic_elements"].append({
            "variable": m.group(1),
            "description": f"Dynamically added view: {m.group(1)}",
        })

    # 7. Extract startActivity navigation targets
    for m in re.finditer(r'Intent\s*\(\s*\w+\s*,\s*(\w+)\s*::\s*class\.java\s*\)', content):
        target = m.group(1)
        result["actions"].append({
            "view_id": "_navigation",
            "event": "startActivity",
            "navigates_to": target,
            "description": f"Navigates to {target}",
        })

    # launchActivity<XxxActivity>
    for m in re.finditer(r'launchActivity\s*<\s*(\w+)\s*>', content):
        target = m.group(1)
        result["actions"].append({
            "view_id": "_navigation",
            "event": "startActivity",
            "navigates_to": target,
            "description": f"Navigates to {target}",
        })

    return result


def _camel_to_snake(name):
    """Convert camelCase to snake_case."""
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()


def _analyze_click_body(body, view_id, event="onClick"):
    """Analyze the body of a click handler to determine the action."""
    action = {
        "view_id": view_id,
        "event": event,
        "description": "",
        "navigates_to": None,
    }

    # Check for startActivity / launchActivity
    m = re.search(r'Intent\s*\(\s*\w+\s*,\s*(\w+)\s*::\s*class\.java', body)
    if m:
        target = m.group(1)
        action["navigates_to"] = target
        action["description"] = f"Navigates to {target}"
        return action

    m = re.search(r'launchActivity\s*<\s*(\w+)\s*>', body)
    if m:
        target = m.group(1)
        action["navigates_to"] = target
        action["description"] = f"Navigates to {target}"
        return action

    # Check for finish()
    if re.search(r'\bfinish\s*\(\s*\)', body):
        action["description"] = "Closes this activity"
        action["navigates_to"] = "[Back]"
        return action

    # Check for dialog
    m = re.search(r'(\w+Dialog)\s*\(', body)
    if m:
        action["description"] = f"Opens {m.group(1)}"
        action["navigates_to"] = f"{m.group(1)} (dialog)"
        return action

    # Check for common operations
    if re.search(r'share|Share', body):
        action["description"] = "Shares content"
    elif re.search(r'delete|Delete|remove|Remove', body):
        action["description"] = "Deletes item"
    elif re.search(r'edit|Edit|crop|Crop', body):
        action["description"] = "Opens editor"
    elif re.search(r'copy|Copy', body):
        action["description"] = "Copies item"
    elif re.search(r'move|Move', body):
        action["description"] = "Moves item"
    elif re.search(r'rename|Rename', body):
        action["description"] = "Renames item"
    elif re.search(r'sort|Sort', body):
        action["description"] = "Changes sort order"
    elif re.search(r'filter|Filter', body):
        action["description"] = "Filters content"
    elif re.search(r'refresh|Refresh', body):
        action["description"] = "Refreshes content"
    elif re.search(r'toggle|Toggle|switch|Switch', body):
        action["description"] = "Toggles state"
    else:
        # Generic
        action["description"] = f"Click handler on {view_id}"

    return action


def scan_adapters(source_dirs):
    """Scan adapter source files to extract item layout mappings.

    Returns: { adapter_class_name: { "source_file": ..., "item_layouts": [...], "recycler_view_id": ... } }
    """
    adapters = {}
    # Pattern: XxxBinding.inflate(layoutInflater, parent, false)
    binding_pattern = re.compile(r'(\w+Binding)\.inflate\s*\(')
    # Pattern: class XxxAdapter
    class_pattern = re.compile(r'class\s+(\w+Adapter)')

    for source_dir in source_dirs:
        if not os.path.isdir(source_dir):
            continue
        for root, dirs, files in os.walk(source_dir):
            for f in files:
                if not (f.endswith(".kt") or f.endswith(".java")):
                    continue
                filepath = os.path.join(root, f)
                try:
                    with open(filepath, "r", encoding="utf-8", errors="ignore") as fh:
                        content = fh.read()
                except Exception:
                    continue

                # Only process adapter files
                class_match = class_pattern.search(content)
                if not class_match:
                    continue

                adapter_name = class_match.group(1)
                item_layouts = []

                # Find all ViewBinding inflate calls → derive layout names
                for bm in binding_pattern.finditer(content):
                    binding_class = bm.group(1)
                    if binding_class.endswith("Binding"):
                        layout_name = re.sub(r'(?<!^)(?=[A-Z])', '_', binding_class[:-7]).lower()
                        if layout_name not in item_layouts:
                            item_layouts.append(layout_name)

                if item_layouts:
                    adapters[adapter_name] = {
                        "source_file": filepath,
                        "item_layouts": item_layouts,
                    }

    return adapters


def scan_fragments(source_dirs):
    """Scan fragment source files to extract layout mappings.

    Returns: { fragment_class_name: { "source_file": ..., "layout_file": ..., "binding_class": ... } }
    """
    fragments = {}
    class_pattern = re.compile(r'class\s+(\w+Fragment)\s*[:(]')
    binding_pattern = re.compile(r'(\w+Binding)\.inflate\s*\(\s*inflater')
    # Also match: binding = XxxBinding.inflate(inflater, container, false)
    binding_pattern2 = re.compile(r'(\w+Binding)\.inflate\s*\(')

    for source_dir in source_dirs:
        if not os.path.isdir(source_dir):
            continue
        for root, dirs, files in os.walk(source_dir):
            for f in files:
                if not (f.endswith(".kt") or f.endswith(".java")):
                    continue
                if "Fragment" not in f:
                    continue
                filepath = os.path.join(root, f)
                try:
                    with open(filepath, "r", encoding="utf-8", errors="ignore") as fh:
                        content = fh.read()
                except Exception:
                    continue

                class_match = class_pattern.search(content)
                if not class_match:
                    continue

                fragment_name = class_match.group(1)
                # Skip abstract base fragments
                if re.search(r'abstract\s+class\s+' + re.escape(fragment_name), content):
                    continue

                binding_match = binding_pattern.search(content) or binding_pattern2.search(content)
                layout_file = None
                binding_class = None
                if binding_match:
                    binding_class = binding_match.group(1)
                    if binding_class.endswith("Binding"):
                        layout_file = re.sub(r'(?<!^)(?=[A-Z])', '_', binding_class[:-7]).lower()

                # Also try onCreateView with R.layout.xxx
                layout_match = re.search(r'inflate\s*\(\s*R\.layout\.(\w+)', content)
                if layout_match and not layout_file:
                    layout_file = layout_match.group(1)

                if layout_file:
                    fragments[fragment_name] = {
                        "source_file": filepath,
                        "layout_file": layout_file,
                        "binding_class": binding_class,
                    }

    return fragments


def scan_dialog_layouts(source_dirs):
    """Scan dialog source files to extract their custom layout mappings.

    Returns: { dialog_class_name: { "source_file": ..., "layout_file": ..., "binding_class": ... } }
    """
    dialogs = {}
    class_pattern = re.compile(r'class\s+(\w+Dialog)')
    # Pattern: DialogXxxBinding.inflate(activity.layoutInflater)
    binding_pattern = re.compile(r'(\w+Binding)\.inflate\s*\(')
    # Pattern: inflate(R.layout.xxx
    layout_pattern = re.compile(r'inflate\s*\(\s*R\.layout\.(\w+)')

    for source_dir in source_dirs:
        if not os.path.isdir(source_dir):
            continue
        for root, dirs, files in os.walk(source_dir):
            for f in files:
                if not (f.endswith(".kt") or f.endswith(".java")):
                    continue
                if "Dialog" not in f:
                    continue
                filepath = os.path.join(root, f)
                try:
                    with open(filepath, "r", encoding="utf-8", errors="ignore") as fh:
                        content = fh.read()
                except Exception:
                    continue

                class_match = class_pattern.search(content)
                if not class_match:
                    continue

                dialog_name = class_match.group(1)
                layout_file = None
                binding_class = None

                binding_match = binding_pattern.search(content)
                if binding_match:
                    binding_class = binding_match.group(1)
                    if binding_class.endswith("Binding"):
                        layout_file = re.sub(r'(?<!^)(?=[A-Z])', '_', binding_class[:-7]).lower()

                layout_match = layout_pattern.search(content)
                if layout_match and not layout_file:
                    layout_file = layout_match.group(1)

                if layout_file:
                    dialogs[dialog_name] = {
                        "source_file": filepath,
                        "layout_file": layout_file,
                        "binding_class": binding_class,
                    }

    return dialogs


def analyze_menu_xml(menu_file, menu_dirs, resolver=None):
    """Parse a menu XML file to extract menu items."""
    menu_path = None
    for d in menu_dirs:
        candidate = os.path.join(d, menu_file + ".xml")
        if os.path.isfile(candidate):
            menu_path = candidate
            break

    if not menu_path:
        return {"menu_file": menu_file, "items": [], "error": "File not found"}

    try:
        tree = ET.parse(menu_path)
    except ET.ParseError as e:
        return {"menu_file": menu_file, "items": [], "error": str(e)}

    import xml.etree.ElementTree as ET
    ANDROID_NS = "http://schemas.android.com/apk/res/android"
    APP_NS = "http://schemas.android.com/apk/res-auto"

    root = tree.getroot()
    items = _parse_menu_items(root, resolver)

    return {"menu_file": menu_file, "items": items}


def _parse_menu_items(parent, resolver=None):
    """Recursively parse menu items."""
    ANDROID_NS = "http://schemas.android.com/apk/res/android"
    APP_NS = "http://schemas.android.com/apk/res-auto"

    items = []
    for elem in parent:
        tag = elem.tag
        if "}" in tag:
            tag = tag.split("}", 1)[1]

        if tag == "item":
            item_id = elem.get(f"{{{ANDROID_NS}}}id", "")
            if item_id:
                item_id = item_id.replace("@+id/", "").replace("@id/", "")

            title = elem.get(f"{{{ANDROID_NS}}}title", "")
            if resolver and title.startswith("@string/"):
                title = resolver.resolve(title)

            icon = elem.get(f"{{{ANDROID_NS}}}icon", "")
            show = elem.get(f"{{{APP_NS}}}showAsAction",
                           elem.get(f"{{{ANDROID_NS}}}showAsAction", ""))
            visible = elem.get(f"{{{ANDROID_NS}}}visible", "true")

            item = {
                "id": item_id,
                "title": title,
                "icon": icon or None,
                "show_as_action": show or None,
                "visible": visible != "false",
            }

            # Check for submenu
            submenu = elem.find("menu")
            if submenu is not None:
                item["submenu"] = _parse_menu_items(submenu, resolver)

            items.append(item)
        elif tag == "group":
            items.extend(_parse_menu_items(elem, resolver))
        elif tag == "menu":
            items.extend(_parse_menu_items(elem, resolver))

    return items


def main():
    if len(sys.argv) < 3:
        print("Usage: analyze_source.py <android_project_root> <discovery_json> [output_dir]")
        sys.exit(1)

    project_root = os.path.abspath(sys.argv[1])
    discovery_path = os.path.abspath(sys.argv[2])
    output_dir = os.path.abspath(sys.argv[3]) if len(sys.argv) > 3 else os.path.join(project_root, "UI_Analysis")
    os.makedirs(output_dir, exist_ok=True)

    # Load discovery
    with open(discovery_path, "r") as f:
        discovery = json.load(f)

    # Collect menu directories
    menu_dirs = []
    for base in [os.path.join(project_root, "app", "src", "main", "res"),
                 os.path.join(project_root, "..", "Simple-Commons", "commons", "src", "main", "res")]:
        menu_dir = os.path.join(base, "menu")
        if os.path.isdir(os.path.normpath(menu_dir)):
            menu_dirs.append(os.path.normpath(menu_dir))

    # Build resource resolver for menu titles
    sys.path.insert(0, os.path.dirname(__file__))
    from parse_layout_xml import ResourceResolver
    resolver = ResourceResolver()
    res_dirs = [os.path.join(project_root, "app", "src", "main", "res")]
    commons_res = os.path.normpath(os.path.join(project_root, "..", "Simple-Commons", "commons", "src", "main", "res"))
    if os.path.isdir(commons_res):
        res_dirs.append(commons_res)
    resolver.load_resources(res_dirs)

    # Analyze each activity
    analyses_dir = os.path.join(output_dir, "source_analyses")
    os.makedirs(analyses_dir, exist_ok=True)

    # Collect all source directories for scanning adapters/fragments/dialogs
    app_src = os.path.join(project_root, "app", "src")
    all_source_dirs = []
    for flavor in ["main", "proprietary", "foss", "prepaid"]:
        for lang in ["kotlin", "java"]:
            d = os.path.join(app_src, flavor, lang)
            if os.path.isdir(d):
                all_source_dirs.append(d)
    # Include Simple-Commons sources
    commons_src = os.path.normpath(os.path.join(project_root, "..", "Simple-Commons", "commons", "src", "main", "kotlin"))
    if os.path.isdir(commons_src):
        all_source_dirs.append(commons_src)

    # Scan adapters, fragments, and dialogs globally
    print("Scanning adapters...")
    adapter_map = scan_adapters(all_source_dirs)
    print(f"  Found {len(adapter_map)} adapters: {', '.join(adapter_map.keys())}")

    print("Scanning fragments...")
    fragment_map = scan_fragments(all_source_dirs)
    print(f"  Found {len(fragment_map)} fragments: {', '.join(fragment_map.keys())}")

    print("Scanning dialog layouts...")
    dialog_layout_map = scan_dialog_layouts(all_source_dirs)
    print(f"  Found {len(dialog_layout_map)} dialogs with layouts: {', '.join(dialog_layout_map.keys())}")

    # Write global maps for use by merge phase
    global_map_path = os.path.join(output_dir, "component_maps.json")
    with open(global_map_path, "w", encoding="utf-8") as f:
        json.dump({
            "adapters": adapter_map,
            "fragments": fragment_map,
            "dialog_layouts": dialog_layout_map,
        }, f, indent=2, ensure_ascii=False)
    print(f"Component maps: {global_map_path}")

    for activity in discovery["activities"]:
        source_file = activity.get("source_file")
        if not source_file:
            continue

        simple_name = activity["simple_name"]
        print(f"Analyzing: {simple_name}")

        analysis = analyze_activity_source(source_file)
        analysis["activity_class"] = activity["class"]
        analysis["simple_name"] = simple_name

        # Parse referenced menus
        for i, menu in enumerate(analysis["menus"]):
            menu_detail = analyze_menu_xml(menu["menu_file"], menu_dirs, resolver)
            analysis["menus"][i] = menu_detail

        # Write analysis — merge with existing Phase 3b data if present
        out_path = os.path.join(analyses_dir, f"{simple_name}.json")
        if os.path.isfile(out_path):
            with open(out_path, "r") as f:
                existing = json.load(f)
            # Preserve Phase 3b fields that Phase 3 can't produce
            for key in ("adapter_bindings", "fragment_containers"):
                if key in existing and existing[key] and key not in analysis:
                    analysis[key] = existing[key]
                elif key in existing and existing[key] and not analysis.get(key):
                    analysis[key] = existing[key]
        with open(out_path, "w", encoding="utf-8") as f:
            json.dump(analysis, f, indent=2, ensure_ascii=False)

    # Also analyze extra source-only activities
    for extra in discovery.get("extra_source_only", []):
        source_file = extra.get("source_file")
        if not source_file:
            continue
        simple_name = extra["class"].rsplit(".", 1)[-1]
        print(f"Analyzing (extra): {simple_name}")

        analysis = analyze_activity_source(source_file)
        analysis["activity_class"] = extra["class"]
        analysis["simple_name"] = simple_name

        for i, menu in enumerate(analysis["menus"]):
            menu_detail = analyze_menu_xml(menu["menu_file"], menu_dirs, resolver)
            analysis["menus"][i] = menu_detail

        out_path = os.path.join(analyses_dir, f"{simple_name}.json")
        with open(out_path, "w", encoding="utf-8") as f:
            json.dump(analysis, f, indent=2, ensure_ascii=False)

    print(f"\nSource analyses written to: {analyses_dir}/")
    return 0


if __name__ == "__main__":
    sys.exit(main())