from __future__ import annotations

import os
import json
import re
import time
import uuid
from dataclasses import dataclass, asdict
from datetime import datetime
from urllib.parse import urlparse

import requests
from flask import Flask, jsonify, render_template, request, send_file, send_from_directory
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from urllib3.exceptions import InsecureRequestWarning


APP_ROOT = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(APP_ROOT, "data")
os.makedirs(DATA_DIR, exist_ok=True)

SCREENSHOT_DIR = os.path.join(DATA_DIR, "screenshots")
os.makedirs(SCREENSHOT_DIR, exist_ok=True)

REPORT_FILE = os.path.join(DATA_DIR, "reports.json")
COMPANY_LOGO_PATH = r"C:\Users\HP\.cursor\projects\C-Users-HP-AppData-Local-Temp-94bf1c92-75bc-4ee9-92ea-d04956682f91\assets\c__Users_HP_AppData_Roaming_Cursor_User_workspaceStorage_1776146302392_images_image-4e274747-8f8e-4dda-99ed-a17ddb54e8d4.png"


@dataclass
class CheckResult:
    url: str
    normalized_url: str
    status_code: int | None
    final_url: str | None
    is_working: bool
    issue: str | None
    screenshot: str | None
    response_time_ms: int | None
    ui_summary: str | None


def load_reports() -> list[dict]:
    if not os.path.exists(REPORT_FILE):
        return []
    try:
        with open(REPORT_FILE, "r", encoding="utf-8") as file:
            data = json.load(file)
        if isinstance(data, list):
            return data
    except Exception:  # noqa: BLE001
        pass
    return []


def save_reports(reports: list[dict]) -> None:
    with open(REPORT_FILE, "w", encoding="utf-8") as file:
        json.dump(reports, file, indent=2)


def slugify(value: str) -> str:
    cleaned = re.sub(r"[^a-zA-Z0-9]+", "-", (value or "").strip().lower()).strip("-")
    return cleaned or "default-app"


def list_all_screenshots() -> list[dict]:
    items: list[dict] = []
    if not os.path.exists(SCREENSHOT_DIR):
        return items
    for root, _, files in os.walk(SCREENSHOT_DIR):
        for file_name in files:
            if not file_name.lower().endswith((".png", ".jpg", ".jpeg", ".webp")):
                continue
            full_path = os.path.join(root, file_name)
            rel_path = os.path.relpath(full_path, SCREENSHOT_DIR).replace("\\", "/")
            parts = rel_path.split("/")
            app_name = parts[0] if len(parts) > 1 else "default-app"
            site_name = parts[1] if len(parts) > 2 else "unknown-site"
            items.append(
                {
                    "app_slug": app_name,
                    "site_slug": site_name,
                    "file_name": file_name,
                    "image_url": f"/screenshots/{rel_path}",
                    "modified_at": datetime.utcfromtimestamp(os.path.getmtime(full_path)).isoformat() + "Z",
                }
            )
    items.sort(key=lambda x: x.get("modified_at") or "", reverse=True)
    return items


def build_screenshot_index(reports: list[dict]) -> dict[str, dict]:
    index: dict[str, dict] = {}
    for run in reports:
        for item in run.get("results", []) or []:
            shot = item.get("screenshot")
            if not shot:
                continue
            index[shot] = {
                "run_id": run.get("id"),
                "run_at": run.get("run_at"),
                "app_name": run.get("app_name"),
                "app_slug": run.get("app_slug"),
                "url": item.get("normalized_url") or item.get("url"),
                "status_code": item.get("status_code"),
                "issue": item.get("issue"),
                "ui_summary": item.get("ui_summary"),
            }
    return index


def screenshot_url_to_path(screenshot_url: str) -> str | None:
    prefix = "/screenshots/"
    if not screenshot_url or not screenshot_url.startswith(prefix):
        return None
    rel_path = screenshot_url[len(prefix) :].replace("/", os.sep)
    full_path = os.path.abspath(os.path.join(SCREENSHOT_DIR, rel_path))
    screenshot_root = os.path.abspath(SCREENSHOT_DIR)
    if not full_path.startswith(screenshot_root):
        return None
    return full_path


def delete_screenshot_file_and_unlink(image_url: str) -> tuple[bool, str | None]:
    full_path = screenshot_url_to_path(image_url)
    if not full_path:
        return False, "Invalid screenshot path."
    if not os.path.exists(full_path):
        return False, "Screenshot not found."

    os.remove(full_path)

    reports = load_reports()
    updated = False
    for run in reports:
        for item in run.get("results", []) or []:
            if item.get("screenshot") == image_url:
                item["screenshot"] = None
                updated = True
    if updated:
        save_reports(reports)
    return True, None


def normalize_url(raw_url: str) -> str:
    raw_url = raw_url.strip()
    if not raw_url:
        return raw_url
    if not raw_url.startswith(("http://", "https://")):
        return f"https://{raw_url}"
    return raw_url


def validate_url(url: str) -> bool:
    parsed = urlparse(url)
    return bool(parsed.scheme and parsed.netloc)


def make_driver() -> webdriver.Chrome:
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--window-size=1920,1080")
    options.add_argument("--disable-gpu")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    return webdriver.Chrome(options=options)


def first_interactable(driver: webdriver.Chrome, css_candidates: list[str]):
    for selector in css_candidates:
        try:
            elements = driver.find_elements(By.CSS_SELECTOR, selector)
        except Exception:  # noqa: BLE001
            continue
        for element in elements:
            try:
                if element.is_displayed() and element.is_enabled():
                    return element
            except Exception:  # noqa: BLE001
                continue
    return None


def login_if_needed(driver: webdriver.Chrome, login_config: dict) -> tuple[bool, str | None]:
    enabled = bool(login_config.get("enabled"))
    if not enabled:
        return True, None

    login_url = normalize_url(str(login_config.get("login_url", "")).strip())
    username = str(login_config.get("username", "")).strip()
    password = str(login_config.get("password", "")).strip()

    if not login_url or not username or not password:
        return False, "Login is enabled but login URL / username / password is missing."

    if not validate_url(login_url):
        return False, "Login URL format is invalid."

    wait = WebDriverWait(driver, 30)

    try:
        driver.get(login_url)
        wait.until(lambda d: d.execute_script("return document.readyState") == "complete")

        username_input = first_interactable(
            driver,
            [
                "input[type='email']",
                "input[name*='email' i]",
                "input[name*='user' i]",
                "input[id*='email' i]",
                "input[id*='user' i]",
                "input[type='text']",
            ],
        )
        password_input = first_interactable(
            driver,
            [
                "input[type='password']",
                "input[name*='pass' i]",
                "input[id*='pass' i]",
            ],
        )
        submit_btn = first_interactable(
            driver,
            [
                "button[type='submit']",
                "input[type='submit']",
                "button[id*='login' i]",
                "button[name*='login' i]",
                "button[class*='login' i]",
                "button",
            ],
        )

        if not username_input or not password_input:
            return False, "Could not auto-detect username/password fields on login page."
        if not submit_btn:
            return False, "Could not auto-detect login button on login page."

        username_input.clear()
        username_input.send_keys(username)
        password_input.clear()
        password_input.send_keys(password)
        submit_btn.click()

        wait.until(lambda d: d.current_url != login_url)
        return True, None
    except TimeoutException:
        return False, "Login timed out. Check credentials or login URL."
    except Exception as exc:  # noqa: BLE001
        return False, f"Login failed: {exc}"


def discover_urls(driver: webdriver.Chrome, seed_url: str, max_pages: int = 25) -> list[str]:
    seed = normalize_url(seed_url)
    if not validate_url(seed):
        return []
    base_host = urlparse(seed).netloc
    queue = [seed]
    visited: set[str] = set()
    discovered: list[str] = []

    while queue and len(discovered) < max_pages:
        current = queue.pop(0)
        if current in visited:
            continue
        visited.add(current)
        try:
            driver.get(current)
            time.sleep(1)
            current_url = driver.current_url
            if current_url not in discovered:
                discovered.append(current_url)
            anchors = driver.find_elements(By.CSS_SELECTOR, "a[href]")
            for anchor in anchors:
                href = (anchor.get_attribute("href") or "").strip()
                if not href:
                    continue
                if href.startswith(("mailto:", "tel:", "javascript:")):
                    continue
                parsed = urlparse(href)
                if parsed.netloc != base_host:
                    continue
                if href not in visited and href not in queue and len(discovered) + len(queue) < max_pages * 2:
                    queue.append(href)
        except Exception:  # noqa: BLE001
            continue
    return discovered


def run_ui_scan(driver: webdriver.Chrome) -> tuple[bool, str]:
    links = driver.find_elements(By.CSS_SELECTOR, "a")
    buttons = driver.find_elements(By.CSS_SELECTOR, "button, input[type='button'], input[type='submit']")
    controls = driver.find_elements(By.CSS_SELECTOR, "input, select, textarea")

    broken_links = 0
    inactive_buttons = 0
    hidden_controls = 0

    for link in links:
        href = (link.get_attribute("href") or "").strip()
        if not href or href.startswith("javascript:"):
            broken_links += 1

    for button in buttons:
        try:
            if (not button.is_displayed()) or (not button.is_enabled()):
                inactive_buttons += 1
        except Exception:  # noqa: BLE001
            inactive_buttons += 1

    for control in controls:
        try:
            if not control.is_displayed():
                hidden_controls += 1
        except Exception:  # noqa: BLE001
            hidden_controls += 1

    has_issues = broken_links > 0
    summary = (
        f"UI scan: links={len(links)} (invalid href={broken_links}), "
        f"buttons={len(buttons)} (inactive={inactive_buttons}), "
        f"controls={len(controls)} (hidden={hidden_controls})"
    )
    return (not has_issues), summary


def detect_page_issue(driver: webdriver.Chrome) -> str | None:
    title_text = (driver.title or "").strip().lower()
    body_text = (driver.page_source or "").lower()

    known_error_titles = {
        "404 not found",
        "page not found",
        "403 forbidden",
        "500 internal server error",
        "bad gateway",
        "service unavailable",
    }
    if title_text in known_error_titles:
        return f"Error page title detected: {driver.title}"

    if "whitelabel error page" in body_text or "exception report" in body_text:
        return "Server error content found in page body"

    return None


def check_with_requests(url: str, timeout: int = 20) -> tuple[int | None, str | None, int | None, str | None]:
    started = time.perf_counter()
    try:
        response = requests.get(url, timeout=timeout, allow_redirects=True)
        elapsed_ms = int((time.perf_counter() - started) * 1000)
        issue = None if response.status_code < 400 else f"HTTP {response.status_code}"
        return response.status_code, response.url, elapsed_ms, issue
    except requests.exceptions.SSLError:
        # Some Windows setups miss intermediate/root certs; fallback keeps the checker usable.
        requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
        try:
            response = requests.get(url, timeout=timeout, allow_redirects=True, verify=False)
            elapsed_ms = int((time.perf_counter() - started) * 1000)
            issue = None if response.status_code < 400 else f"HTTP {response.status_code}"
            return response.status_code, response.url, elapsed_ms, issue
        except requests.RequestException as exc:
            elapsed_ms = int((time.perf_counter() - started) * 1000)
            return None, None, elapsed_ms, str(exc)
    except requests.RequestException as exc:
        elapsed_ms = int((time.perf_counter() - started) * 1000)
        return None, None, elapsed_ms, str(exc)


def capture_screenshot(driver: webdriver.Chrome, url: str) -> tuple[str | None, str | None]:
    filename = f"{uuid.uuid4().hex}.png"
    full_path = os.path.join(SCREENSHOT_DIR, filename)
    try:
        driver.get(url)
        time.sleep(2)
        page_text = (driver.page_source or "").lower()
        title_text = (driver.title or "").lower()
        issue = None
        if "404" in page_text or "page not found" in page_text or "not found" in title_text:
            issue = "404-like content detected in page"
        driver.save_screenshot(full_path)
        return f"/screenshots/{filename}", issue
    except WebDriverException as exc:
        return None, f"Browser error: {exc.msg}"


def capture_screenshot_for_app(
    driver: webdriver.Chrome,
    url: str,
    app_slug: str,
) -> tuple[str | None, str | None]:
    parsed = urlparse(url)
    site_slug = slugify(parsed.netloc or "unknown-site")
    site_folder = os.path.join(SCREENSHOT_DIR, app_slug, site_slug)
    os.makedirs(site_folder, exist_ok=True)
    filename = f"{int(time.time())}_{uuid.uuid4().hex[:10]}.png"
    full_path = os.path.join(site_folder, filename)

    try:
        driver.get(url)
        time.sleep(2)
        issue = detect_page_issue(driver)
        driver.save_screenshot(full_path)
        return f"/screenshots/{app_slug}/{site_slug}/{filename}", issue
    except WebDriverException as exc:
        return None, f"Browser error: {exc.msg}"


def run_single_check(driver: webdriver.Chrome, raw_url: str, app_slug: str) -> CheckResult:
    normalized = normalize_url(raw_url)
    if not normalized:
        return CheckResult(
            url=raw_url,
            normalized_url=normalized,
            status_code=None,
            final_url=None,
            is_working=False,
            issue="Empty URL",
            screenshot=None,
            response_time_ms=None,
            ui_summary=None,
        )

    if not validate_url(normalized):
        return CheckResult(
            url=raw_url,
            normalized_url=normalized,
            status_code=None,
            final_url=None,
            is_working=False,
            issue="Invalid URL format",
            screenshot=None,
            response_time_ms=None,
            ui_summary=None,
        )

    status_code, final_url, response_time_ms, req_issue = check_with_requests(normalized)
    screenshot, browser_issue = capture_screenshot_for_app(driver, normalized, app_slug)
    ui_ok, ui_summary = run_ui_scan(driver)

    final_issue = req_issue or browser_issue
    is_working = final_issue is None and status_code is not None and status_code < 400
    if final_issue is None and not ui_ok:
        final_issue = "Invalid link href values found in page"

    return CheckResult(
        url=raw_url,
        normalized_url=normalized,
        status_code=status_code,
        final_url=final_url,
        is_working=is_working,
        issue=final_issue,
        screenshot=screenshot,
        response_time_ms=response_time_ms,
        ui_summary=ui_summary,
    )


app = Flask(__name__, static_folder="static", template_folder="templates")


@app.get("/")
def index():
    return render_template("index.html")


@app.get("/screenshots/<path:filename>")
def get_screenshot(filename: str):
    return send_from_directory(SCREENSHOT_DIR, filename)


@app.get("/company-logo")
def get_company_logo():
    if os.path.exists(COMPANY_LOGO_PATH):
        return send_file(COMPANY_LOGO_PATH)
    return "", 404


@app.get("/api/apps")
def list_apps():
    reports = load_reports()
    app_map = {}
    for item in reports:
        app_name = item.get("app_name", "Unnamed App")
        app_slug = item.get("app_slug", slugify(app_name))
        if app_name not in app_map:
            app_map[app_name] = {
                "app_name": app_name,
                "app_slug": app_slug,
                "runs": 0,
                "last_run_at": item.get("run_at"),
            }
        app_map[app_name]["runs"] += 1
        if item.get("run_at", "") > (app_map[app_name].get("last_run_at") or ""):
            app_map[app_name]["last_run_at"] = item.get("run_at")
    apps = sorted(app_map.values(), key=lambda x: x.get("last_run_at") or "", reverse=True)
    return jsonify({"apps": apps})


@app.get("/api/report")
def get_report():
    reports = load_reports()
    app_slug = (request.args.get("app_slug") or "").strip()
    if app_slug:
        reports = [item for item in reports if item.get("app_slug") == app_slug]
    reports.sort(key=lambda x: x.get("run_at") or "", reverse=True)
    return jsonify({"reports": reports})


@app.get("/api/report/<run_id>")
def get_report_run(run_id: str):
    reports = load_reports()
    for run in reports:
        if run.get("id") == run_id:
            return jsonify({"run": run})
    return jsonify({"error": "Run not found"}), 404


@app.get("/api/screenshots")
def get_screenshots():
    app_slug = (request.args.get("app_slug") or "").strip()
    reports = load_reports()
    shot_index = build_screenshot_index(reports)
    items = list_all_screenshots()
    if app_slug:
        items = [item for item in items if item.get("app_slug") == app_slug]
    for item in items:
        meta = shot_index.get(item.get("image_url", ""))
        if meta:
            item.update(meta)
    return jsonify({"screenshots": items})


@app.delete("/api/screenshots")
def delete_screenshot():
    body = request.get_json(silent=True) or {}
    image_url = str(body.get("image_url", "")).strip()
    ok, error = delete_screenshot_file_and_unlink(image_url)
    if not ok:
        return jsonify({"error": error}), 400 if error == "Invalid screenshot path." else 404

    return jsonify({"success": True})


@app.post("/api/screenshots/bulk-delete")
def bulk_delete_screenshots():
    body = request.get_json(silent=True) or {}
    image_urls = body.get("image_urls", [])
    if not isinstance(image_urls, list) or not image_urls:
        return jsonify({"error": "Provide non-empty image_urls list."}), 400

    deleted = 0
    errors: list[dict] = []
    for image_url in image_urls:
        ok, error = delete_screenshot_file_and_unlink(str(image_url))
        if ok:
            deleted += 1
        else:
            errors.append({"image_url": image_url, "error": error})
    return jsonify({"success": True, "deleted": deleted, "errors": errors})


@app.delete("/api/report/<run_id>")
def delete_report_run(run_id: str):
    reports = load_reports()
    target = None
    remaining = []
    for run in reports:
        if run.get("id") == run_id:
            target = run
        else:
            remaining.append(run)
    if not target:
        return jsonify({"error": "Run not found"}), 404

    for item in target.get("results", []) or []:
        full_path = screenshot_url_to_path(str(item.get("screenshot") or ""))
        if full_path and os.path.exists(full_path):
            try:
                os.remove(full_path)
            except OSError:
                pass

    save_reports(remaining)
    return jsonify({"success": True})


@app.post("/api/report/bulk-delete")
def bulk_delete_report_runs():
    body = request.get_json(silent=True) or {}
    run_ids = body.get("run_ids", [])
    if not isinstance(run_ids, list) or not run_ids:
        return jsonify({"error": "Provide non-empty run_ids list."}), 400

    reports = load_reports()
    run_id_set = {str(rid) for rid in run_ids}
    targets = [run for run in reports if run.get("id") in run_id_set]
    if not targets:
        return jsonify({"error": "No matching runs found"}), 404

    for run in targets:
        for item in run.get("results", []) or []:
            full_path = screenshot_url_to_path(str(item.get("screenshot") or ""))
            if full_path and os.path.exists(full_path):
                try:
                    os.remove(full_path)
                except OSError:
                    pass

    remaining = [run for run in reports if run.get("id") not in run_id_set]
    save_reports(remaining)
    return jsonify({"success": True, "deleted": len(targets)})


@app.delete("/api/apps/<app_slug>")
def delete_app_history(app_slug: str):
    app_slug = (app_slug or "").strip()
    if not app_slug:
        return jsonify({"error": "Invalid app slug"}), 400

    reports = load_reports()
    remaining = [run for run in reports if run.get("app_slug") != app_slug]

    if len(remaining) == len(reports):
        return jsonify({"error": "Application not found"}), 404

    app_folder = os.path.join(SCREENSHOT_DIR, app_slug)
    if os.path.exists(app_folder):
        for root, _, files in os.walk(app_folder, topdown=False):
            for file_name in files:
                try:
                    os.remove(os.path.join(root, file_name))
                except OSError:
                    pass
            try:
                os.rmdir(root)
            except OSError:
                pass

    save_reports(remaining)
    return jsonify({"success": True})


@app.post("/api/apps/bulk-delete")
def bulk_delete_app_history():
    body = request.get_json(silent=True) or {}
    app_slugs = body.get("app_slugs", [])
    if not isinstance(app_slugs, list) or not app_slugs:
        return jsonify({"error": "Provide non-empty app_slugs list."}), 400

    slug_set = {str(slug).strip() for slug in app_slugs if str(slug).strip()}
    if not slug_set:
        return jsonify({"error": "No valid app slugs provided"}), 400

    reports = load_reports()
    remaining = [run for run in reports if run.get("app_slug") not in slug_set]
    if len(remaining) == len(reports):
        return jsonify({"error": "No matching apps found"}), 404

    for app_slug in slug_set:
        app_folder = os.path.join(SCREENSHOT_DIR, app_slug)
        if os.path.exists(app_folder):
            for root, _, files in os.walk(app_folder, topdown=False):
                for file_name in files:
                    try:
                        os.remove(os.path.join(root, file_name))
                    except OSError:
                        pass
                try:
                    os.rmdir(root)
                except OSError:
                    pass

    save_reports(remaining)
    return jsonify({"success": True, "deleted_apps": len(slug_set)})


@app.post("/api/check")
def check_urls():
    body = request.get_json(silent=True) or {}
    urls = body.get("urls", [])
    login_config = body.get("login", {})
    app_name = str(body.get("app_name", "")).strip() or "Unnamed App"
    app_slug = slugify(app_name)
    auto_discover = bool(body.get("auto_discover"))
    discover_from = str(body.get("discover_from", "")).strip()
    max_discover_pages = int(body.get("max_discover_pages", 25) or 25)

    if not isinstance(urls, list):
        return jsonify({"error": "Invalid urls format."}), 400

    driver = None
    try:
        driver = make_driver()
        logged_in, login_issue = login_if_needed(driver, login_config if isinstance(login_config, dict) else {})
        if not logged_in:
            return jsonify({"error": login_issue or "Login failed."}), 400
        if auto_discover:
            if not discover_from:
                return jsonify({"error": "Provide a base URL for auto discover."}), 400
            urls = discover_urls(driver, discover_from, max_pages=max(1, min(max_discover_pages, 150)))
        if not urls:
            return jsonify({"error": "No URLs to test. Add URLs or use auto discover."}), 400
        results = [asdict(run_single_check(driver, str(u), app_slug)) for u in urls]
    except Exception as exc:  # noqa: BLE001
        return jsonify({"error": f"Failed to run checker: {exc}"}), 500
    finally:
        if driver:
            driver.quit()

    total = len(results)
    working = len([r for r in results if r.get("is_working")])
    issues = total - working
    run_record = {
        "id": uuid.uuid4().hex,
        "app_name": app_name,
        "app_slug": app_slug,
        "run_at": datetime.utcnow().isoformat() + "Z",
        "total_urls": total,
        "working_urls": working,
        "issue_urls": issues,
        "results": results,
    }
    reports = load_reports()
    reports.append(run_record)
    save_reports(reports)

    return jsonify({"results": results})


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=True)
