import urllib.request
import urllib.error
import urllib.parse
import concurrent.futures
import ssl
import time
import json
import csv
import re
import io
import os

# ─── Internal helpers ────────────────────────────────────────────────

def _check_ssl(url):
    """Check if a URL has a valid SSL certificate."""
    try:
        parsed = urllib.parse.urlparse(url)
        if parsed.scheme != "https":
            return None  # not applicable
        ctx = ssl.create_default_context()
        conn = ctx.wrap_socket(
            __import__("socket").create_connection((parsed.hostname, 443), timeout=10),
            server_hostname=parsed.hostname
        )
        conn.close()
        return True
    except ssl.SSLCertVerificationError:
        return False
    except Exception:
        return None

def _check_url(url, retries=1):
    """Check a single URL and return a full result dict."""
    attempt = 0
    last_result = None

    while attempt <= retries:
        try:
            start = time.time()
            req = urllib.request.Request(url, headers={"User-Agent": "linksnap/2.0"})
            ctx = ssl.create_default_context()
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE

            with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
                elapsed = round((time.time() - start) * 1000)
                final_url = r.geturl()
                code = r.status
                ssl_ok = _check_ssl(url)

                if final_url.rstrip("/") != url.rstrip("/"):
                    last_result = {
                        "url": url, "status": "REDIRECT", "code": code,
                        "redirect": final_url, "response_ms": elapsed, "ssl": ssl_ok
                    }
                else:
                    last_result = {
                        "url": url, "status": "ALIVE", "code": code,
                        "redirect": None, "response_ms": elapsed, "ssl": ssl_ok
                    }
                return last_result

        except urllib.error.HTTPError as e:
            elapsed = round((time.time() - start) * 1000)
            last_result = {
                "url": url, "status": "DEAD", "code": e.code,
                "redirect": None, "response_ms": elapsed, "ssl": None
            }
        except urllib.error.URLError:
            last_result = {
                "url": url, "status": "DEAD", "code": None,
                "redirect": None, "response_ms": None, "ssl": None
            }
        except Exception:
            last_result = {
                "url": url, "status": "ERROR", "code": None,
                "redirect": None, "response_ms": None, "ssl": None
            }

        attempt += 1

    return last_result

def _run_checks(urls, workers=10, retries=1):
    """Run checks concurrently and return sorted results."""
    results = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
        futures = {executor.submit(_check_url, url, retries): url for url in urls}
        for future in concurrent.futures.as_completed(futures):
            results.append(future.result())
    order = {url: i for i, url in enumerate(urls)}
    results.sort(key=lambda r: order.get(r["url"], 0))
    return results

def _print_report(results):
    """Print a formatted report of results."""
    alive = dead = redirected = errors = 0

    for r in results:
        url = r["url"]
        status = r["status"]
        code = r["code"]
        redirect = r["redirect"]
        ms = r["response_ms"]
        ssl_ok = r["ssl"]

        code_str = f" ({code})" if code else ""
        ms_str = f" [{ms}ms]" if ms is not None else ""
        ssl_str = ""
        if ssl_ok is False:
            ssl_str = " ⚠️ SSL"
        elif ssl_ok is True:
            ssl_str = " 🔒"

        if status == "ALIVE":
            print(f"  ✅ ALIVE{code_str}{ms_str}{ssl_str}     {url}")
            alive += 1
        elif status == "REDIRECT":
            print(f"  ↪️  REDIRECT{code_str}{ms_str}  {url}")
            print(f"               ↳ {redirect}")
            redirected += 1
        elif status == "DEAD":
            print(f"  ❌ DEAD{code_str}{ms_str}      {url}")
            dead += 1
        else:
            print(f"  ⚠️  ERROR{ms_str}       {url}")
            errors += 1

    total = alive + dead + redirected + errors
    print(f"\n  📊 {total} checked  |  {alive} alive  |  {redirected} redirected  |  {dead} dead  |  {errors} errors")

# ─── Public API ──────────────────────────────────────────────────────

def check(urls, workers=10, retries=1):
    """
    Check a list of URLs. Returns a list of result dicts.

    Each result has:
      - url         : the original URL
      - status      : ALIVE, DEAD, REDIRECT, or ERROR
      - code        : HTTP status code (or None)
      - redirect    : final URL if redirected (or None)
      - response_ms : response time in milliseconds (or None)
      - ssl         : True (valid), False (invalid), or None (not applicable)

    Args:
        urls     : a single URL string or list of URL strings
        workers  : number of concurrent threads (default 10)
        retries  : how many times to retry dead links (default 1)
    """
    if isinstance(urls, str):
        urls = [urls]
    return _run_checks(urls, workers=workers, retries=retries)


def snap(urls, workers=10, retries=1):
    """
    Check a list of URLs and print a formatted report.

    Args:
        urls     : a single URL string or list of URL strings
        workers  : number of concurrent threads (default 10)
        retries  : how many times to retry dead links (default 1)
    """
    if isinstance(urls, str):
        urls = [urls]
    print(f"\n🔗 linksnap — checking {len(urls)} URL(s)...\n")
    results = _run_checks(urls, workers=workers, retries=retries)
    _print_report(results)
    print()
    return results


def scan_page(url, workers=10, retries=1):
    """
    Fetch a webpage and check all links found on it.

    Args:
        url      : the webpage URL to scan
        workers  : number of concurrent threads (default 10)
        retries  : how many times to retry dead links (default 1)
    """
    print(f"\n🌐 linksnap — scanning page: {url}\n")
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "linksnap/2.0"})
        with urllib.request.urlopen(req, timeout=10) as r:
            html = r.read().decode("utf-8", errors="ignore")
    except Exception as e:
        print(f"  ❌ Could not fetch page: {e}\n")
        return []

    raw_links = re.findall(r'href=["\']?(https?://[^\s"\'<>]+)', html)
    links = list(dict.fromkeys(raw_links))  # deduplicate, preserve order

    print(f"  🔍 Found {len(links)} unique links — checking...\n")
    results = _run_checks(links, workers=workers, retries=retries)
    _print_report(results)
    print()
    return results


def from_file(path, workers=10, retries=1):
    """
    Read URLs from a .txt, .csv, or .html file and check them all.

    - .txt  : one URL per line
    - .csv  : scans all cells for URLs
    - .html : extracts all href links

    Args:
        path     : path to the file
        workers  : number of concurrent threads (default 10)
        retries  : how many times to retry dead links (default 1)
    """
    ext = os.path.splitext(path)[1].lower()

    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        content = f.read()

    if ext == ".txt":
        urls = [line.strip() for line in content.splitlines()
                if line.strip().startswith("http")]
    elif ext == ".csv":
        urls = re.findall(r'https?://[^\s",<>]+', content)
    elif ext in (".html", ".htm"):
        urls = re.findall(r'href=["\']?(https?://[^\s"\'<>]+)', content)
    else:
        # Try to extract any URLs from unknown file types
        urls = re.findall(r'https?://[^\s"\'<>]+', content)

    urls = list(dict.fromkeys(urls))  # deduplicate
    print(f"\n📂 linksnap — loaded {len(urls)} URL(s) from {os.path.basename(path)}\n")
    results = _run_checks(urls, workers=workers, retries=retries)
    _print_report(results)
    print()
    return results


def to_json(results, path):
    """
    Export results to a JSON file.

    Args:
        results : list of result dicts from check(), snap(), etc.
        path    : output file path (e.g. 'results.json')
    """
    with open(path, "w", encoding="utf-8") as f:
        json.dump(results, f, indent=2)
    print(f"💾 Saved {len(results)} results to {path}")


def to_csv(results, path):
    """
    Export results to a CSV file.

    Args:
        results : list of result dicts from check(), snap(), etc.
        path    : output file path (e.g. 'results.csv')
    """
    fieldnames = ["url", "status", "code", "redirect", "response_ms", "ssl"]
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(results)
    print(f"💾 Saved {len(results)} results to {path}")