"""
MYOB Supporting Documents flow (skeleton).

This module defines the entry point for the MYOB supporting documents
flow. Initially mirrors the QuickBooks flow with MYOB-specific labels and
environment variable prefixes so we can iterate safely.
"""


def _scroll_to_bottom() -> None:
    """Scroll to the bottom of the page using keyboard and wheel fallbacks."""
    try:
        import time as _time
        import pyautogui as _pg
        # Primary: Ctrl+End reaches very bottom in most browsers/apps
        try:
            _pg.hotkey('ctrl', 'end')
            _time.sleep(0.2)
        except Exception:
            pass
        # Secondary: press End as additional nudge
        try:
            _pg.press('end')
            _time.sleep(0.1)
        except Exception:
            pass
        # Fallback: a few strong wheel scroll downs
        try:
            for _ in range(6):
                _pg.scroll(-800)  # negative -> down
                _time.sleep(0.05)
        except Exception:
            pass
    except Exception:
        pass


def _focus_content_region() -> None:
    """Bring focus to the web page by clicking near the screen center."""
    try:
        import pyautogui as _pg
        import time as _time
        try:
            sw, sh = _pg.size()
        except Exception:
            sw, sh = 1920, 1080
        cx = int(max(10, sw // 2))
        cy = int(max(10, sh // 2))
        _pg.moveTo(cx, cy)
        _pg.click()
        _time.sleep(0.1)
    except Exception:
        pass


def play_supporting_docs(app) -> None:
    """Entry point for the MYOB supporting documents flow."""
    # Step 1: Scroll to the bottom where attachments are located
    try:
        if hasattr(app, "status_var"):
            app.status_var.set("MYOB: scrolling to bottom...")
    except Exception:
        pass
    # Ensure the web content has focus before scrolling
    _focus_content_region()
    _scroll_to_bottom()
    # Allow UI to settle after scroll
    try:
        import time as _time
        _time.sleep(2.0)
    except Exception:
        pass

    # Step 2: Find attachment links under the "Attachments" section using OpenAI
    try:
        if hasattr(app, "status_var"):
            app.status_var.set("MYOB: analyzing for attachment links...")
    except Exception:
        pass

    # Acquire analyzer
    try:
        analyzer = getattr(app, 'screenshot_manager', None)
        analyzer = getattr(analyzer, 'openai_analyzer', None)
        if analyzer is None:
            if hasattr(app, 'status_var'):
                app.status_var.set("Analyzer unavailable")
            return
    except Exception:
        try:
            if hasattr(app, 'status_var'):
                app.status_var.set("Analyzer unavailable")
        except Exception:
            pass
        return

    # Capture screenshot (ensure focus, then small delay to allow UI settle after scroll)
    try:
        import time as _time
        # Ensure the web content has focus before capture
        _focus_content_region()
        _time.sleep(0.4)
        screenshot_path = analyzer.capture_full_resolution_screenshot()
        if not screenshot_path:
            if hasattr(app, 'status_var'):
                app.status_var.set("Failed to capture screenshot")
            return
    except Exception:
        try:
            if hasattr(app, 'status_var'):
                app.status_var.set("Failed to capture screenshot")
        except Exception:
            pass
        return

    # Build MYOB-specific "Attachments" links prompt
    def _myob_attachments_links_prompt(width: int, height: int) -> str:
        sw = int(width); sh = int(height)
        _tmpl = (
            """
                Analyze this MYOB screenshot and identify ONLY supporting document links that appear
                under the 'Attachments' section/heading (also consider common misspellings: 'Attachements', 'Attatchements').

                Look for:
                - The clickable file names (e.g., Invoice 1010.pdf) in the list under Attachments
                - Download/View links or buttons for receipts, PDFs, or supporting documents in that list only
                - File attachment icons (paperclip, document, PDF icons) for those files

                Return ONLY a JSON array with this exact format:
                [
                    {
                        "type": "supporting_document_link",
                        "description": "brief description of what the link provides",
                        "link_words": "the exact visible text (verbatim)",
                        "coordinates": [x, y],
                        "confidence": 0.95,
                        "button": true
                    }
                ]

                Rules:
                - Include links ONLY if they are visually under the Attachments heading/label
                - link_words MUST be the exact clickable text; for icon-only, set link_words to ""
                - button: true for buttons/icons; false for plain text links
                - Coordinates are pixel center of the clickable element (0-__XMAX__ for x, 0-__YMAX__ for y)
                - EXCLUDE anything not part of the file list, including: "Add attachment", "Select All",
                  "Attach to email", remove "x" icons, size labels (e.g., "15KB"), and section helper text
                - Return valid JSON only, no extra commentary
                - This screenshot is __SW__x__SH__ pixels
            """
        )
        return (
            _tmpl
            .replace("__XMAX__", str(sw - 1))
            .replace("__YMAX__", str(sh - 1))
            .replace("__SW__", str(sw))
            .replace("__SH__", str(sh))
        )

    # Prepare prompt and image
    try:
        sw, sh = analyzer._get_screen_size()
    except Exception:
        sw, sh = 1920, 1080

    prompt = _myob_attachments_links_prompt(sw, sh)

    # Optional OpenAI debug directory for prompt/response logging
    _oai_debug_dir = None
    try:
        import os as _os
        if (_os.getenv('MYOB_OAI_DEBUG', '0') in ('1', 'true', 'True')) or (_os.getenv('MYOB_REKOG_DEBUG', '0') in ('1', 'true', 'True')):
            from pathlib import Path as _Path
            import time as _t
            _oai_debug_dir = _Path('logs') / 'openai_debug' / f"myob_oai_{int(_t.time()*1000)}"
            _oai_debug_dir.mkdir(parents=True, exist_ok=True)
            _oai_debug_dir = str(_oai_debug_dir)
    except Exception:
        _oai_debug_dir = None

    try:
        import base64 as _b64
        with open(screenshot_path, 'rb') as _f:
            _img_b = _f.read()
        _img_b64 = _b64.b64encode(_img_b).decode('utf-8')
        image_url = f"data:image/png;base64,{_img_b64}"
    except Exception:
        if hasattr(app, 'status_var'):
            app.status_var.set("Failed to prepare screenshot")
        return

    # Call OpenAI vision
    try:
        try:
            from .constants import OPENAI_VISION_TEMPERATURE
        except Exception:
            OPENAI_VISION_TEMPERATURE = 0.0

        content = analyzer._post_vision(
            prompt,
            image_url,
            max_tokens=1600,
            temperature=float(OPENAI_VISION_TEMPERATURE),
        )
        # Log prompt and raw response when debug is enabled
        try:
            if _oai_debug_dir:
                import os as _os
                _p_path = _os.path.join(_oai_debug_dir, 'prompt.txt')
                _r_path = _os.path.join(_oai_debug_dir, 'response.txt')
                with open(_p_path, 'w', encoding='utf-8') as _pf:
                    _pf.write(str(prompt))
                with open(_r_path, 'w', encoding='utf-8') as _rf:
                    _rf.write(str(content))
        except Exception:
            pass
    except Exception:
        try:
            if hasattr(app, 'status_var'):
                app.status_var.set("Analyze failed")
        except Exception:
            pass
        return

    # Parse JSON array from response
    try:
        import json as _json
        _start = content.find('['); _end = content.rfind(']') + 1
        if _start != -1 and _end != -1:
            _json_str = content[_start:_end]
            _links = _json.loads(_json_str)
        else:
            _links = []
        # Log parsed links if debug enabled
        try:
            if _oai_debug_dir:
                import os as _os
                _links_path = _os.path.join(_oai_debug_dir, 'parsed_links.json')
                with open(_links_path, 'w', encoding='utf-8') as _lf:
                    _lf.write(_json.dumps(_links, ensure_ascii=False, indent=2))
        except Exception:
            pass
    except Exception:
        _links = []

    # Validate and normalize
    validated_links = []
    for link in (_links or []):
        if not isinstance(link, dict):
            continue
        if 'link_words' not in link:
            try:
                desc = str(link.get('description') or '').strip()
                link['link_words'] = desc
            except Exception:
                link['link_words'] = ''
        if 'button' not in link:
            link['button'] = False
        try:
            coords = link.get('coordinates', [])
            if analyzer._validate_coordinates(coords):
                validated_links.append(link)
        except Exception:
            continue

    # Filter out known non-file entries that should not be considered supporting docs
    try:
        _exclude_words = {
            'add attachment', 'select all', 'attach to email', 'max file size', 'size', 'kb', 'mb',
        }
        _filtered_links = []
        for _lnk in validated_links:
            try:
                _w = str(_lnk.get('link_words') or '').strip().lower()
            except Exception:
                _w = ''
            # Exclude exact helper/control texts; allow filenames like *.pdf, *.jpg, etc.
            if any(word in _w for word in _exclude_words):
                continue
            _filtered_links.append(_lnk)
        validated_links = _filtered_links
    except Exception:
        pass

    # Expose for subsequent steps and decide continuation
    try:
        setattr(app, '_myob_attachment_links', validated_links)
    except Exception:
        pass

    if not validated_links:
        try:
            if hasattr(app, 'status_var'):
                app.status_var.set("No attachment links found")
        except Exception:
            pass
        return

    # Click sequentially with a short wait between, no screenshots or extra actions
    try:
        if hasattr(app, 'status_var'):
            app.status_var.set(f"Clicking {len(validated_links)} attachment link(s) sequentially...")
    except Exception:
        pass

    # Determine wait between clicks
    try:
        import os as _os
        import time as _time
        try:
            wait_between = float(_os.getenv('MYOB_CLICK_WAIT_SEC', '2.0'))
        except Exception:
            wait_between = 2.0
    except Exception:
        # Fallback if imports fail
        wait_between = 2.0
        import time as _time

    # Track clicked coordinates to avoid re-clicking the same location
    clicked_points = []  # list of (x,y)
    try:
        dedup_radius = float(_os.getenv('MYOB_DEDUP_RADIUS_PX', '28'))
    except Exception:
        dedup_radius = 28.0

    def _is_near_clicked(pt) -> bool:
        try:
            if not isinstance(pt, (list, tuple)) or len(pt) != 2:
                return False
            px, py = float(pt[0]), float(pt[1])
            for (cx, cy) in clicked_points:
                dx = px - cx; dy = py - cy
                if (dx*dx + dy*dy) ** 0.5 <= dedup_radius:
                    return True
        except Exception:
            return False
        return False

    # Prepare Rekognition client and PIL image once
    try:
        from PIL import Image as _MYOBImg
        import boto3 as _b3
        from .rekognition_tiler import find_text_coordinates_tiled as _myob_tiler
        from .constants import AWS_REGION as _MYOB_AWS_REGION
        _myob_img = _MYOBImg.open(screenshot_path).convert('RGB')
        _myob_rk = _b3.client('rekognition', region_name=_MYOB_AWS_REGION)
    except Exception:
        _myob_img = None
        _myob_rk = None
        _myob_tiler = None

    # Tiler parameters
    try:
        _upscale = float(_os.getenv('MYOB_REKOGNITION_UPSCALE', '2.0'))
    except Exception:
        _upscale = 2.0
    try:
        _overlap = float(_os.getenv('MYOB_REKOGNITION_TILE_OVERLAP', '0.10'))
    except Exception:
        _overlap = 0.10
    _debug_dir = None
    try:
        if _os.getenv('MYOB_REKOG_DEBUG', '0') in ('1', 'true', 'True'):
            from pathlib import Path as _Path
            import time as _t
            _debug_dir = _Path('logs') / 'openai_debug' / f"rekognition_myob_{int(_t.time()*1000)}"
            _debug_dir.mkdir(parents=True, exist_ok=True)
            _debug_dir = str(_debug_dir)
    except Exception:
        _debug_dir = None

    # Helper: try to locate the close "x" icon on the same row and aim left to the download icon
    def _maybe_adjust_to_download_icon(center_x: int, center_y: int, dbg_dir: str = None):
        try:
            # Feature switch
            use_adjust = str(_os.getenv('MYOB_USE_X_LEFT_OF_CLOSE', '1')).lower() in ('1', 'true')
        except Exception:
            use_adjust = True
        if not use_adjust:
            return None
        if _myob_img is None or _myob_rk is None:
            return None
        try:
            from io import BytesIO as _BytesIO
            import json as _json
            _buf = _BytesIO()
            # JPEG compress to stay under Rekognition size limits
            _myob_img.save(_buf, format='JPEG', quality=75)
            _img_bytes = _buf.getvalue()
            resp = _myob_rk.detect_text(Image={'Bytes': _img_bytes})
            # Optionally log full response
            try:
                _dbg = dbg_dir or _debug_dir
                if _dbg:
                    _dt_path = _os.path.join(_dbg, 'detect_text.json')
                    with open(_dt_path, 'w', encoding='utf-8') as _f:
                        _json.dump(resp, _f, indent=2)
            except Exception:
                pass

            # Parameters
            try:
                row_tol = float(_os.getenv('MYOB_ROW_Y_TOLERANCE', '28'))
            except Exception:
                row_tol = 28.0
            try:
                shift_left = float(_os.getenv('MYOB_X_TO_DOWNLOAD_ICON_OFFSET_PX', '34'))
            except Exception:
                shift_left = 38.0
            # Candidate source policy: 'full' (default), 'tiles', or 'both'
            try:
                cand_source = str(_os.getenv('MYOB_X_CANDIDATE_SOURCE', 'tiles')).strip().lower()
                if cand_source not in ('full', 'tiles', 'both'):
                    cand_source = 'full'
            except Exception:
                cand_source = 'full'

            W, H = _myob_img.size
            candidates = []
            cand_details = []
            # Collect diagnostics for all 'x'-like detections (for debugging why some are rejected)
            _all_x_detections = []
            try:
                for _det in (resp or {}).get('TextDetections', []) or []:
                    try:
                        _t = str((_det.get('DetectedText') or '').strip())
                        if _t not in ('x', 'X', '×'):
                            continue
                        if str((_det.get('Type') or '')).upper() != 'WORD':
                            _det_type = str(_det.get('Type') or '')
                        else:
                            _det_type = 'WORD'
                        _geom = _det.get('Geometry') or {}
                        _box = _geom.get('BoundingBox') or {}
                        _cx = int((float(_box.get('Left', 0)) + float(_box.get('Width', 0)) / 2.0) * W)
                        _cy = int((float(_box.get('Top', 0)) + float(_box.get('Height', 0)) / 2.0) * H)
                        _all_x_detections.append({
                            'text': _t,
                            'type': _det_type,
                            'cx': int(_cx),
                            'cy': int(_cy),
                            'right_of_center': bool(int(_cx) > int(center_x)),
                            'row_ok': bool(abs(int(_cy) - int(center_y)) <= row_tol),
                        })
                    except Exception:
                        continue
            except Exception:
                _all_x_detections = []
            for det in ((resp or {}).get('TextDetections', []) or []) if cand_source != 'tiles' else []:
                try:
                    # Only consider single-character WORD detections
                    if str(det.get('Type') or '').upper() != 'WORD':
                        continue
                    txt = str(det.get('DetectedText') or '').strip()
                    # Accept only lowercase 'x' exactly
                    if txt != 'x':
                        continue
                    geom = det.get('Geometry') or {}
                    box = geom.get('BoundingBox') or {}
                    cx = int((float(box.get('Left', 0)) + float(box.get('Width', 0)) / 2.0) * W)
                    cy = int((float(box.get('Top', 0)) + float(box.get('Height', 0)) / 2.0) * H)
                    bw = float(box.get('Width', 0)) * W
                    bh = float(box.get('Height', 0)) * H
                    # Must be to the right of the link text and on roughly the same row
                    if cx <= int(center_x):
                        continue
                    if abs(cy - int(center_y)) > row_tol:
                        continue
                    candidates.append((cx, cy))
                    cand_details.append({
                        'cx': int(cx), 'cy': int(cy),
                        'bbox_w': int(bw), 'bbox_h': int(bh),
                        'dx_from_center': int(cx - int(center_x)),
                        'dy_from_center': int(cy - int(center_y)),
                        'source': 'full'
                    })
                except Exception:
                    continue
            # Optionally add tile-derived candidates
            tile_candidates = []
            tile_cand_details = []
            if cand_source in ('tiles', 'both'):
                try:
                    _dbg = dbg_dir or _debug_dir
                    _tiles_path = _os.path.join(_dbg, 'raw_detect_text_all_tiles.json') if _dbg else None
                    if _tiles_path and _os.path.exists(_tiles_path):
                        import json as _json
                        with open(_tiles_path, 'r', encoding='utf-8') as _tf:
                            _tiles_data = _json.load(_tf) or []
                        for _tile in _tiles_data:
                            try:
                                _t_left = int(_tile.get('tile_left') or 0)
                                _t_top = int(_tile.get('tile_top') or 0)
                                _scale = float(_tile.get('upscale') or 1.0)
                                _iw = int((_tile.get('width') or 0) or 0)
                                _ih = int((_tile.get('height') or 0) or 0)
                                for _d in (_tile.get('response', {}) or {}).get('TextDetections', []) or []:
                                    try:
                                        if str((_d.get('Type') or '')).upper() != 'WORD':
                                            continue
                                        _txt = str((_d.get('DetectedText') or '').strip())
                                        if _txt != 'x':
                                            continue
                                        _bb = (_d.get('Geometry') or {}).get('BoundingBox') or {}
                                        _cx = int((float(_bb.get('Left', 0)) + float(_bb.get('Width', 0)) / 2.0) * max(1, _iw))
                                        _cy = int((float(_bb.get('Top', 0)) + float(_bb.get('Height', 0)) / 2.0) * max(1, _ih))
                                        gx = int(_t_left + (_cx / max(1.0, _scale)))
                                        gy = int(_t_top + (_cy / max(1.0, _scale)))
                                        bw = float(_bb.get('Width', 0)) * max(1, _iw) / max(1.0, _scale)
                                        bh = float(_bb.get('Height', 0)) * max(1, _ih) / max(1.0, _scale)
                                        if gx <= int(center_x):
                                            continue
                                        if abs(gy - int(center_y)) > row_tol:
                                            continue
                                        tile_candidates.append((gx, gy))
                                        tile_cand_details.append({
                                            'cx': int(gx), 'cy': int(gy),
                                            'bbox_w': int(bw), 'bbox_h': int(bh),
                                            'dx_from_center': int(gx - int(center_x)),
                                            'dy_from_center': int(gy - int(center_y)),
                                            'source': 'tiles'
                                        })
                                    except Exception:
                                        continue
                            except Exception:
                                continue
                except Exception:
                    pass
            # Merge/override per source policy
            if cand_source == 'tiles':
                candidates = tile_candidates
                cand_details = tile_cand_details
            elif cand_source == 'both':
                try:
                    candidates = candidates + tile_candidates
                    cand_details = cand_details + tile_cand_details
                except Exception:
                    pass
            if not candidates:
                # Log empty candidate list when debug is enabled for diagnostics
                try:
                    _dbg = dbg_dir or _debug_dir
                    if _dbg:
                        _cand_path = _os.path.join(_dbg, 'x_candidates.json')
                        import json as _json
                        _cand_payload = {
                            "link_center": [int(center_x), int(center_y)],
                            "candidates": cand_details,
                            "selection_rule": "closest_right_by_dx_then_dy",
                            "source_used": cand_source,
                            "counts": {
                                "full": len([c for c in cand_details if c.get('source') == 'full']),
                                "tiles": len([c for c in cand_details if c.get('source') == 'tiles'])
                            },
                            "note": "no lowercase 'x' WORD candidates to the right on same row"
                        }
                        with open(_cand_path, 'w', encoding='utf-8') as _f:
                            _f.write(_json.dumps(_cand_payload, indent=2))
                        # Also log all observed 'x' detections with reasons
                        _all_path = _os.path.join(_dbg, 'x_detections_all.json')
                        with open(_all_path, 'w', encoding='utf-8') as _af:
                            _af.write(_json.dumps({
                                'link_center': [int(center_x), int(center_y)],
                                'row_tolerance': int(row_tol),
                                'detections': _all_x_detections,
                                'filter': {
                                    'type': 'WORD only',
                                    'text_equals': "x",
                                    'right_of_center': True,
                                    'row_tolerance': int(row_tol)
                                }
                            }, indent=2))
                        # Convert and log tile detections to absolute coordinates even when no candidates
                        try:
                            _tiles_path = _os.path.join(_dbg, 'raw_detect_text_all_tiles.json')
                            if _os.path.exists(_tiles_path):
                                with open(_tiles_path, 'r', encoding='utf-8') as _tf:
                                    _tiles_data = _json.load(_tf)
                                _tile_x_abs = []
                                for _tile in (_tiles_data or []):
                                    try:
                                        _t_left = int(_tile.get('tile_left') or 0)
                                        _t_top = int(_tile.get('tile_top') or 0)
                                        _scale = float(_tile.get('upscale') or 1.0)
                                        _iw = int((_tile.get('width') or 0) or 0)
                                        _ih = int((_tile.get('height') or 0) or 0)
                                        for _d in (_tile.get('response', {}) or {}).get('TextDetections', []) or []:
                                            try:
                                                _txt = str((_d.get('DetectedText') or '').strip())
                                                _typ = str((_d.get('Type') or '')).upper()
                                                if _typ not in ('WORD', 'LINE'):
                                                    continue
                                                if _txt not in ('x', 'X', '×'):
                                                    continue
                                                _bb = (_d.get('Geometry') or {}).get('BoundingBox') or {}
                                                _cx = int((float(_bb.get('Left', 0)) + float(_bb.get('Width', 0)) / 2.0) * max(1, _iw))
                                                _cy = int((float(_bb.get('Top', 0)) + float(_bb.get('Height', 0)) / 2.0) * max(1, _ih))
                                                _gx = int(_t_left + (_cx / max(1.0, _scale)))
                                                _gy = int(_t_top + (_cy / max(1.0, _scale)))
                                                _tile_x_abs.append({
                                                    'text': _txt,
                                                    'type': _typ,
                                                    'global_x': _gx,
                                                    'global_y': _gy,
                                                    'right_of_center': bool(_gx > int(center_x)),
                                                    'row_ok': bool(abs(_gy - int(center_y)) <= row_tol),
                                                    'tile_left': _t_left,
                                                    'tile_top': _t_top,
                                                    'upscale': _scale,
                                                })
                                            except Exception:
                                                continue
                                    except Exception:
                                        continue
                                with open(_os.path.join(_dbg, 'x_tile_candidates_abs.json'), 'w', encoding='utf-8') as _taf:
                                    _taf.write(_json.dumps({
                                        'link_center': [int(center_x), int(center_y)],
                                        'detections': _tile_x_abs,
                                        'note': 'Tile detections converted to absolute screen coords (no selection logic applied)'
                                    }, indent=2))
                        except Exception:
                            pass
                except Exception:
                    pass
                return None
            # Log the candidate list prior to selection when debug is enabled
            try:
                _dbg = dbg_dir or _debug_dir
                if _dbg:
                    _cand_path = _os.path.join(_dbg, 'x_candidates.json')
                    import json as _json
                    _cand_payload = {
                        "link_center": [int(center_x), int(center_y)],
                        "candidates": cand_details,
                        "selection_rule": "closest_right_by_dx_then_dy",
                        "source_used": cand_source,
                        "counts": {
                            "full": len([c for c in cand_details if c.get('source') == 'full']),
                            "tiles": len([c for c in cand_details if c.get('source') == 'tiles'])
                        }
                    }
                    with open(_cand_path, 'w', encoding='utf-8') as _f:
                        _f.write(_json.dumps(_cand_payload, indent=2))
                    # Additionally, if tiled raw detections exist, convert and log their absolute coords
                    try:
                        _tiles_path = _os.path.join(_dbg, 'raw_detect_text_all_tiles.json')
                        if _os.path.exists(_tiles_path):
                            with open(_tiles_path, 'r', encoding='utf-8') as _tf:
                                _tiles_data = _json.load(_tf)
                            _tile_x_abs = []
                            for _tile in (_tiles_data or []):
                                try:
                                    _t_left = int(_tile.get('tile_left') or 0)
                                    _t_top = int(_tile.get('tile_top') or 0)
                                    _scale = float(_tile.get('upscale') or 1.0)
                                    _iw = int((_tile.get('width') or 0) or 0)
                                    _ih = int((_tile.get('height') or 0) or 0)
                                    for _d in (_tile.get('response', {}) or {}).get('TextDetections', []) or []:
                                        try:
                                            _txt = str((_d.get('DetectedText') or '').strip())
                                            _typ = str((_d.get('Type') or '')).upper()
                                            if _typ not in ('WORD', 'LINE'):
                                                continue
                                            # Log x-like tokens only
                                            if _txt not in ('x', 'X', '×'):
                                                continue
                                            _bb = (_d.get('Geometry') or {}).get('BoundingBox') or {}
                                            _cx = int((float(_bb.get('Left', 0)) + float(_bb.get('Width', 0)) / 2.0) * max(1, _iw))
                                            _cy = int((float(_bb.get('Top', 0)) + float(_bb.get('Height', 0)) / 2.0) * max(1, _ih))
                                            _gx = int(_t_left + (_cx / max(1.0, _scale)))
                                            _gy = int(_t_top + (_cy / max(1.0, _scale)))
                                            _tile_x_abs.append({
                                                'text': _txt,
                                                'type': _typ,
                                                'global_x': _gx,
                                                'global_y': _gy,
                                                'right_of_center': bool(_gx > int(center_x)),
                                                'row_ok': bool(abs(_gy - int(center_y)) <= row_tol),
                                                'tile_left': _t_left,
                                                'tile_top': _t_top,
                                                'upscale': _scale,
                                            })
                                        except Exception:
                                            continue
                                except Exception:
                                    continue
                            with open(_os.path.join(_dbg, 'x_tile_candidates_abs.json'), 'w', encoding='utf-8') as _taf:
                                _taf.write(_json.dumps({
                                    'link_center': [int(center_x), int(center_y)],
                                    'detections': _tile_x_abs,
                                    'note': 'Tile detections converted to absolute screen coords (no selection logic applied)'
                                }, indent=2))
                    except Exception:
                        pass
            except Exception:
                pass
            # Choose the closest 'x' to the right
            candidates.sort(key=lambda p: (abs(p[0] - int(center_x)), abs(p[1] - int(center_y))))
            # Also log the sorted order with distances for clarity
            try:
                _dbg = dbg_dir or _debug_dir
                if _dbg:
                    _sorted_path = _os.path.join(_dbg, 'x_candidates_sorted.json')
                    import json as _json
                    sorted_list = sorted(cand_details, key=lambda d: (abs(int(d['cx']) - int(center_x)), abs(int(d['cy']) - int(center_y))))
                    with open(_sorted_path, 'w', encoding='utf-8') as _sf:
                        _sf.write(_json.dumps({
                            'link_center': [int(center_x), int(center_y)],
                            'sorted_candidates': sorted_list,
                            'selection_rule': 'closest_right_by_dx_then_dy'
                        }, indent=2))
            except Exception:
                pass
            x_c, y_c = candidates[0]
            target_x = max(0, int(x_c - shift_left))
            target_y = int(y_c)
            # Optionally log chosen point
            try:
                _dbg = dbg_dir or _debug_dir
                if _dbg:
                    _sum_path = _os.path.join(_dbg, 'myob_click_adjustments.json')
                    rec = {"link_center": [int(center_x), int(center_y)], "x_center": [x_c, y_c], "target": [target_x, target_y], "shift_left": int(shift_left)}
                    if _os.path.exists(_sum_path):
                        # append line-delimited JSON
                        with open(_sum_path, 'a', encoding='utf-8') as _f:
                            _f.write(_json.dumps(rec) + "\n")
                    else:
                        with open(_sum_path, 'w', encoding='utf-8') as _f:
                            _f.write(_json.dumps(rec) + "\n")
                    # Also write a one-off file with the chosen candidate
                    try:
                        _chosen_path = _os.path.join(_dbg, 'x_chosen.json')
                        import json as _json
                        with open(_chosen_path, 'w', encoding='utf-8') as _cf:
                            _cf.write(_json.dumps({
                                "chosen": {"cx": int(x_c), "cy": int(y_c)},
                                "target": {"x": int(target_x), "y": int(target_y)},
                                "shift_left": int(shift_left)
                            }, indent=2))
                    except Exception:
                        pass
            except Exception:
                pass
            return [target_x, target_y]
        except Exception:
            return None

    for idx, link in enumerate(validated_links, start=1):
        # Always rely ONLY on Rekognition for clickable coordinates
        # Clear any pre-existing (OpenAI) coordinates to avoid fallback
        try:
            if 'coordinates' in link:
                link['coordinates'] = None
            if '_coords_are_screen' in link:
                del link['_coords_are_screen']
        except Exception:
            pass

        # Prefer Rekognition-derived coordinates using the exact link words
        try:
            lw = str(link.get('link_words') or '').strip()
        except Exception:
            lw = ''
        coords = None
        # Create per-link debug directory when enabled
        _link_debug_dir = None
        try:
            if _debug_dir:
                import os as _os
                _link_debug_dir = _os.path.join(_debug_dir, f"link_{idx}")
                try:
                    import pathlib as _pl
                    _pl.Path(_link_debug_dir).mkdir(parents=True, exist_ok=True)
                except Exception:
                    pass
        except Exception:
            _link_debug_dir = None
        if lw and _myob_img is not None and _myob_rk is not None and _myob_tiler is not None:
            try:
                coords = _myob_tiler(
                    _myob_rk,
                    _myob_img,
                    query=lw,
                    is_regex=False,
                    upscale=_upscale,
                    overlap_frac=_overlap,
                    debug_dir=_link_debug_dir or _debug_dir,
                    require_include=True,
                    exclude_points=clicked_points,
                    exclude_radius=int(dedup_radius),
                )
            except Exception:
                coords = None
        # If Rekognition did not find coordinates, log failure info and skip this link
        if not (isinstance(coords, (list, tuple)) and len(coords) == 2):
            try:
                if _link_debug_dir:
                    import json as _json
                    with open(_os.path.join(_link_debug_dir, 'failure.json'), 'w', encoding='utf-8') as _ff:
                        _ff.write(_json.dumps({
                            'reason': 'rekognition_not_found',
                            'link_words': lw,
                        }, ensure_ascii=False, indent=2))
            except Exception:
                pass
            try:
                if hasattr(app, 'status_var'):
                    app.status_var.set(f"Skipping attachment {idx}: Rekognition did not locate '{(lw or 'link')}'")
            except Exception:
                pass
            continue
        # Set screen-space coords from Rekognition result
        try:
            link['coordinates'] = [int(coords[0]), int(coords[1])]
            link['_coords_are_screen'] = True  # signal no transform needed
        except Exception:
            pass

        # Adjust coordinates to target the download icon (based on locating the close 'x')
        try:
            _lc = link.get('coordinates')
            _adj = _maybe_adjust_to_download_icon(int(_lc[0]), int(_lc[1]), dbg_dir=_link_debug_dir) if isinstance(_lc, (list, tuple)) and len(_lc) == 2 else None
            if isinstance(_adj, (list, tuple)) and len(_adj) == 2:
                link['coordinates'] = [int(_adj[0]), int(_adj[1])]
        except Exception:
            pass

        # Skip if this link's coordinates are near a previously clicked point
        try:
            _coords = link.get('coordinates')
            if _is_near_clicked(_coords):
                continue
        except Exception:
            pass

        try:
            if hasattr(app, 'status_var'):
                app.status_var.set(f"Clicking attachment {idx}/{len(validated_links)}...")
        except Exception:
            pass

        try:
            _res = analyzer._click_link(link)
            try:
                if isinstance(_res, dict) and _res.get('success') and isinstance(_res.get('coordinates'), (list, tuple)) and len(_res.get('coordinates')) == 2:
                    clicked_points.append((int(_res['coordinates'][0]), int(_res['coordinates'][1])))
                else:
                    clicked_points.append((int(coords[0]), int(coords[1])))
            except Exception:
                pass
        except Exception:
            pass

        # After click: wait 2s, then run save dialog once (no subplaylist)
        try:
            _time.sleep(2.0)
        except Exception:
            pass
        try:
            _time.sleep(0.3)
        except Exception:
            pass
        try:
            from .xero_util.attach_files_dialog_utils import run_save_dialog_once_if_present as _myob_save_once
            _myob_save_once(app)
        except Exception:
            pass

        try:
            _time.sleep(wait_between)
        except Exception:
            pass

    try:
        if hasattr(app, 'status_var'):
            app.status_var.set("Attachment link flow complete")
    except Exception:
        pass
    return


