#!/usr/bin/env python3
"""
Link validator for download_links.xlsx
Checks each URL — marks BROKEN if:
  - HTTP status 404 / 5xx / connection error
  - Response HTML contains "ERROR: File path does not exist"
  - Response HTML contains "ERROR:" (directory lister error page)

Usage:
  python3 check_links.py                         # uses download_links.xlsx in same folder
  python3 check_links.py --input path/to/file.xlsx
  python3 check_links.py --workers 30            # parallel threads (default 20)
  python3 check_links.py --timeout 15            # seconds per request (default 10)
"""

import argparse, os, sys, time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

try:
    import openpyxl
    from openpyxl.styles import Font, PatternFill, Alignment
    from openpyxl.utils import get_column_letter
except ImportError:
    sys.exit("pip3 install openpyxl")

try:
    import requests
    from requests.adapters import HTTPAdapter
    from urllib3.util.retry import Retry
except ImportError:
    sys.exit("pip3 install requests")

HERE = Path(__file__).parent

def make_session(timeout):
    s = requests.Session()
    retry = Retry(total=2, backoff_factor=0.3,
                  status_forcelist=[429, 500, 502, 503, 504])
    s.mount('http://',  HTTPAdapter(max_retries=retry))
    s.mount('https://', HTTPAdapter(max_retries=retry))
    s.headers.update({'User-Agent': 'Mozilla/5.0 (compatible; LinkChecker/1.0)'})
    return s

def check_url(url, timeout):
    """Returns (status_code_or_str, is_broken, note)"""
    try:
        r = make_session(timeout).get(url, timeout=timeout, allow_redirects=True,
                                       stream=True)
        code = r.status_code
        
        if code >= 400:
            return code, True, f'HTTP {code}'
        
        # Read up to 8KB to check for error page content
        content = b''
        for chunk in r.iter_content(8192):
            content += chunk
            break
        r.close()
        
        text = content.decode('utf-8', errors='ignore')
        if 'ERROR: File path does not exist' in text or \
           ('<b>ERROR:</b>' in text and 'does not exist' in text):
            return code, True, 'File path does not exist'
        if '<b>ERROR:</b>' in text:
            return code, True, 'Server error page'
        
        return code, False, 'OK'
    
    except requests.exceptions.ConnectionError:
        return 'ERR', True, 'Connection error'
    except requests.exceptions.Timeout:
        return 'TIMEOUT', True, 'Timeout'
    except requests.exceptions.TooManyRedirects:
        return 'REDIR', True, 'Too many redirects'
    except Exception as e:
        return 'ERR', True, str(e)[:60]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--input',   default=str(HERE / 'download_links.xlsx'))
    ap.add_argument('--output',  default=str(HERE / 'link_check_results.xlsx'))
    ap.add_argument('--workers', type=int, default=20)
    ap.add_argument('--timeout', type=int, default=10)
    args = ap.parse_args()

    print(f"Reading {args.input}…")
    wb_in = openpyxl.load_workbook(args.input, read_only=True)
    ws_in = wb_in['Download Links']

    rows_data = []
    for i, row in enumerate(ws_in.iter_rows(values_only=True)):
        if i == 0:
            continue  # header
        if row[5]:    # URL column (index 5)
            rows_data.append(list(row))

    wb_in.close()
    print(f"Loaded {len(rows_data)} links. Starting checks with {args.workers} workers…\n")

    # Check all URLs in parallel
    results = {}   # row_index → (status, is_broken, note)
    urls = [(i, r[5]) for i, r in enumerate(rows_data)]

    done = 0
    t0 = time.time()

    with ThreadPoolExecutor(max_workers=args.workers) as pool:
        futures = {pool.submit(check_url, url, args.timeout): idx
                   for idx, url in urls}
        for fut in as_completed(futures):
            idx = futures[fut]
            status, is_broken, note = fut.result()
            results[idx] = (status, is_broken, note)
            done += 1
            if done % 100 == 0 or done == len(urls):
                elapsed = time.time() - t0
                broken = sum(1 for v in results.values() if v[1])
                pct = done / len(urls) * 100
                print(f"  [{done}/{len(urls)} {pct:.0f}%] broken={broken} "
                      f"elapsed={elapsed:.0f}s  eta={elapsed/done*(len(urls)-done):.0f}s")

    # ── Write output Excel ─────────────────────────────────────────────────────
    print(f"\nWriting results to {args.output}…")
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "All Links"

    GREEN  = PatternFill(start_color='C6EFCE', end_color='C6EFCE', fill_type='solid')
    RED    = PatternFill(start_color='FFC7CE', end_color='FFC7CE', fill_type='solid')
    HFILL  = PatternFill(start_color='1E3A5F', end_color='1E3A5F', fill_type='solid')
    HFONT  = Font(bold=True, color='FFFFFF', size=11)
    RFONT  = Font(bold=True, color='C00000')
    GFONT  = Font(bold=True, color='006100')

    headers = ['Post ID', 'عنوان محتوا', 'عنوان بخش', 'کیفیت', 'حجم',
               'لینک دانلود', 'وضعیت', 'نتیجه']
    for c, h in enumerate(headers, 1):
        cell = ws.cell(row=1, column=c, value=h)
        cell.fill = HFILL; cell.font = HFONT
        cell.alignment = Alignment(horizontal='center', vertical='center')
    ws.row_dimensions[1].height = 24

    for ri, row in enumerate(rows_data):
        r = ri + 2
        status, is_broken, note = results.get(ri, ('?', True, 'unknown'))
        fill = RED if is_broken else GREEN

        for c, val in enumerate(row, 1):
            cell = ws.cell(row=r, column=c, value=val)
            cell.fill = fill
            cell.alignment = Alignment(horizontal='right' if c != 6 else 'left',
                                       wrap_text=(c not in [6,7,8]))
            if c == 6:
                cell.font = Font(color='0563C1')

        ws.cell(row=r, column=7, value=str(status)).fill = fill
        ws.cell(row=r, column=7).alignment = Alignment(horizontal='center')

        note_cell = ws.cell(row=r, column=8, value=note)
        note_cell.fill = fill
        note_cell.font = RFONT if is_broken else GFONT
        note_cell.alignment = Alignment(horizontal='center')

    for i, w in enumerate([10, 36, 28, 26, 10, 70, 10, 22], 1):
        ws.column_dimensions[get_column_letter(i)].width = w
    ws.freeze_panes = 'A2'

    # Broken-only sheet
    ws2 = wb.create_sheet("❌ Broken Links")
    for c, h in enumerate(headers, 1):
        cell = ws2.cell(row=1, column=c, value=h)
        cell.fill = HFILL; cell.font = HFONT
        cell.alignment = Alignment(horizontal='center', vertical='center')
    ws2.row_dimensions[1].height = 24

    br = 2
    for ri, row in enumerate(rows_data):
        status, is_broken, note = results.get(ri, ('?', True, 'unknown'))
        if not is_broken:
            continue
        for c, val in enumerate(row, 1):
            cell = ws2.cell(row=br, column=c, value=val)
            cell.fill = RED
            cell.alignment = Alignment(horizontal='right' if c != 6 else 'left')
            if c == 6:
                cell.font = Font(color='0563C1')
        ws2.cell(row=br, column=7, value=str(status)).fill = RED
        ws2.cell(row=br, column=7).alignment = Alignment(horizontal='center')
        note_cell = ws2.cell(row=br, column=8, value=note)
        note_cell.fill = RED; note_cell.font = RFONT
        note_cell.alignment = Alignment(horizontal='center')
        for i, w in enumerate([10, 36, 28, 26, 10, 70, 10, 22], 1):
            ws2.column_dimensions[get_column_letter(i)].width = w
        br += 1

    ws2.freeze_panes = 'A2'

    # Summary sheet
    ws3 = wb.create_sheet("Summary")
    total   = len(rows_data)
    broken  = sum(1 for v in results.values() if v[1])
    ok      = total - broken
    ws3['A1'] = 'نتایج بررسی لینک‌ها'
    ws3['A1'].font = Font(bold=True, size=14)
    ws3['A3'] = 'کل لینک‌ها';     ws3['B3'] = total
    ws3['A4'] = 'لینک سالم';      ws3['B4'] = ok
    ws3['A5'] = 'لینک خراب';      ws3['B5'] = broken
    ws3['A6'] = 'درصد خرابی';     ws3['B6'] = f'{broken/total*100:.1f}%' if total else '0%'
    ws3['A8'] = 'تاریخ بررسی';    ws3['B8'] = time.strftime('%Y-%m-%d %H:%M')

    wb.save(args.output)
    print(f"\nDone!  Total={total}  OK={ok}  Broken={broken}  ({broken/total*100:.1f}%)")
    print(f"Results saved to: {args.output}")

if __name__ == '__main__':
    main()
