"""
DealerWay reader.

DealerWay (dealerway.co.uk) is a fifth auction platform, added 2026-08-24
(Mark: "similar to a4c and dealer auction"). Real reconnaissance done first
(golden rule 2), logged in with Mark present, captured against the real
Listings screen and one real car's own detail page (data/inspect/
dealerway_tab0.html, dealerway_detail.html).

The site is a React Native Web single page app: every class name is an auto
generated atomic utility ("css-175oi2r", "r-1mtiv16", ...), meaningless and
unstable, so unlike every other reader in this project this one does not
parse raw HTML text at all. Instead it runs a small JS extraction in the
page itself (_EXTRACT_JS) that walks the real DOM structurally (each card is
one real `<a href="/listings/<id>">`, found live, not guessed) and returns
one plain dict per card: its own visible text (line by line, the same shape
already proven reliable for LE Capital's plain text parsing) plus the three
status icons' own real state, read from each icon's background colour
rather than its (also meaningless) class name.

Status icon colours, confirmed against a real detail page (BU19DDL): green
(rgb(0,210,103), a tick shaped path) means the flag is clear, red
(rgb(223,3,3), a cross shaped path) means it is not. Verified two ways:
that same car's own detail page reads "No Bodywork Issues Reported" for a
green Bodywork icon, and a genuine disclosed mechanical fault ("has a
little whistle... boosts well no lights on dash no codes") for a red
Mechanical / Electrical icon. A third colour (blue, or a pale grey) is also
seen and treated as unknown, never guessed as either state.

No numeric reserve is ever shown anywhere, list or detail page (checked,
including the raw HTML for a hidden script value the way Auction4Cars
turned out to have one; none found here). The list card's own Current Bid
(once a bid exists) or Starting Price (before any bid) stands in for
reserve, the same "platform's own real figure, never a guess" reasoning
already used for Auction4Cars and Dealer Auction's own reserve stand ins.

No 1 to 5 condition grade exists either, only these pass/fail flags, so the
grade gate is skipped for this source the same way it already is for
Auction4Cars (assume_grade_ok=True in daily_run.py). Distance IS given
directly and exactly ("209 Miles Away") on every card, so the normal
distance gate applies unlike Auction4Cars or Dealer Auction.

Auctions run on their own per lot countdown (a live "DD:HH:MM:SS" string,
not a real timestamp anywhere in the page), so auction_ends_at is computed
as now() plus that duration at read time, an approximation accurate to
about the few seconds a read takes, the same shape Auction4Cars' own
countdown already uses on the cockpit.

"Bodywork Issues" has no established hard gate rule anywhere else in this
project (only a known MECHANICAL issue is a hard reject, an electrical one
stays a flag), so a bodywork issue here is also kept as a flag (car.notes),
not a rejection, until Mark says otherwise. A known Electrical or Mechanical
issue (DealerWay discloses both under one combined flag, no way to tell
them apart) maps to car.mech_issue, a hard reject, matching the existing
"any known mechanical issue rejects regardless" rule played safe.

Nothing here bids. It reads the screen only.
"""

import re
import datetime
from ..pricing import Car
from .. import browser

LIST_URL = "https://dealerway.co.uk/listings"

MONEY = re.compile(r"£\s*([\d,]+)")
MILES_AWAY = re.compile(r"^(\d+(?:\.\d+)?)\s*Miles Away$")
MILEAGE = re.compile(r"^([\d,]+)\s*miles$")
YEAR = re.compile(r"^(19[89]\d|20[0-4]\d)$")
REG = re.compile(r"^[A-Z0-9]{2,8}$")
COUNTDOWN = re.compile(r"^(\d+):(\d+):(\d+):(\d+)$")
RATING = re.compile(r"^\d(?:\.\d)?\s*\(\d+\)$")
TRANSMISSIONS = {"Manual", "Automatic", "Semi-Automatic", "CVT"}
FUELS = {"Petrol", "Diesel", "Hybrid", "Electric", "Plug-in Hybrid"}

_MULTI_WORD_MAKES = [
    "LAND ROVER", "MERCEDES-BENZ", "MERCEDES BENZ", "ALFA ROMEO",
    "ASTON MARTIN", "GREAT WALL",
]

# The three real status flags read from each card's own icon colour. Order
# matches the fixed order they always render in on a real card.
_STATUS_LABELS = ["Test Driven", "Bodywork", "Mechanical / Electrical"]

_EXTRACT_JS = """
() => {
  const anchors = Array.from(document.querySelectorAll('a[href^="/listings/"]'));
  const labels = ['Test Driven', 'Bodywork', 'Mechanical / Electrical'];
  return anchors.map(a => {
    const iconStates = {};
    labels.forEach(label => {
      const labelEl = Array.from(a.querySelectorAll('div')).find(
        d => d.children.length === 0 && d.textContent.trim() === label);
      if (labelEl) {
        const row = labelEl.parentElement;
        const iconWrap = row.querySelector('div[style*=background-color]');
        const bg = iconWrap ? iconWrap.getAttribute('style') : '';
        iconStates[label] = bg.includes('0, 210, 103') ? 'ok'
          : (bg.includes('223, 3, 3') ? 'issue' : 'unknown');
      } else {
        iconStates[label] = null;
      }
    });
    return {href: a.getAttribute('href'), text: a.innerText, iconStates: iconStates};
  });
}
"""


def _title_case(s):
    return " ".join(w.capitalize() if not w.isupper() or len(w) > 3 else w for w in s.split())


def _split_make_model(title):
    up = (title or "").strip().upper()
    for mk in _MULTI_WORD_MAKES:
        if up.startswith(mk):
            rest = title.strip()[len(mk):].strip()
            return _title_case(mk), _title_case(rest)
    parts = (title or "").strip().split(" ", 1)
    make = parts[0] if parts else ""
    model = parts[1] if len(parts) > 1 else ""
    return _title_case(make), _title_case(model)


def _money(s):
    if not s:
        return None
    m = MONEY.search(s)
    return int(m.group(1).replace(",", "")) if m else None


def _end_time(countdown, now=None):
    m = COUNTDOWN.match(countdown or "")
    if not m:
        return ""
    days, hours, minutes, seconds = (int(x) for x in m.groups())
    now = now or datetime.datetime.now()
    end = now + datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
    return end.replace(microsecond=0).isoformat()


def parse_card(card, now=None):
    """One dict from the real page's own _EXTRACT_JS (href, text, iconStates)
    turned into a Car, or None when it is not really a live auction card (a
    Buy Now only listing has no "LIVE AUCTION" line at all, kept out by the
    auction only rule) or its text does not match the expected shape closely
    enough to trust (never guessed, held back instead)."""
    lines = [ln.strip() for ln in (card.get("text") or "").splitlines() if ln.strip()]
    if not lines or lines[0] != "LIVE AUCTION":
        return None
    if len(lines) < 2:
        return None
    i = 1
    ends_at = _end_time(lines[i], now)
    i += 1

    if i >= len(lines) or not REG.match(lines[i]):
        return None
    reg = lines[i]
    i += 1

    distance_miles = None
    if i < len(lines):
        m = MILES_AWAY.match(lines[i])
        if m:
            distance_miles = float(m.group(1))
            i += 1

    # Seller name, an optional rating line straight after it.
    if i < len(lines):
        i += 1  # seller name, not currently used on the car itself
    if i < len(lines) and RATING.match(lines[i]):
        i += 1

    if i >= len(lines):
        return None
    make, model = _split_make_model(lines[i])
    i += 1
    if i >= len(lines):
        return None
    derivative = lines[i]
    i += 1

    if i < len(lines) and lines[i] == "FRANCHISED DEALER":
        i += 1

    mileage = None
    if i < len(lines):
        m = MILEAGE.match(lines[i])
        if m:
            mileage = int(m.group(1).replace(",", ""))
            i += 1

    transmission = ""
    if i < len(lines) and lines[i] in TRANSMISSIONS:
        transmission = lines[i]
        i += 1
    fuel = ""
    if i < len(lines) and lines[i] in FUELS:
        fuel = lines[i]
        i += 1
    year = None
    if i < len(lines):
        m = YEAR.match(lines[i])
        if m:
            year = int(m.group(1))
            i += 1

    # Skip the three status label lines themselves, their real state came
    # from iconStates, not this text (the text alone cannot tell tick from
    # cross, both just say the plain label).
    for label in _STATUS_LABELS:
        if i < len(lines) and lines[i] == label:
            i += 1

    if i < len(lines) and lines[i] == "Live Auction":
        i += 1

    reserve = None
    if i < len(lines) and lines[i] in ("Current Bid", "Starting Price"):
        i += 1
        if i < len(lines):
            reserve = _money(lines[i])
            i += 1

    icon = card.get("iconStates") or {}
    car = Car(
        reg=reg, make=make, model=model, derivative=derivative,
        year=year, mileage=mileage, reserve=reserve,
        distance_miles=distance_miles, transmission=transmission, fuel=fuel,
        engine=f"{derivative} {fuel}".strip(),
        source="DealerWay",
        listing_url="https://dealerway.co.uk" + card.get("href", ""),
        auction_ends_at=ends_at,
    )
    notes = []
    if icon.get("Bodywork") == "issue":
        notes.append("Bodywork issues reported on the listing.")
    if notes:
        car.notes = " ".join(notes)
    if icon.get("Mechanical / Electrical") == "issue":
        car.mech_issue = "Electrical or mechanical issue disclosed on the listing."
    return car


def parse_listing(cards, today=None, now=None, days_ahead=7):
    """cards is _EXTRACT_JS's own return value (a list of {href, text,
    iconStates} dicts). now is the reference clock for turning a live
    countdown into a real end time, defaults to datetime.now().

    today restricts to lots ending within days_ahead days (None means no
    date filter, used for a plain parse test). Originally kept only today
    or tomorrow, the same narrow window Auction4Cars started with; found
    live 2026-08-24 (Mark: "im showing no available cars however when i
    search the auction site... i have 2 live auctions") that DealerWay's
    own lots genuinely run several days too (a real live check found 3 of
    7 real cards ending 2026-08-27, 3 days out, silently dropped by the
    old window before they ever reached the gate, one of them well within
    range on distance too), so widened to the same 7 day default and the
    same proper INCLUSIVE RANGE check Auction4Cars was fixed to use, not
    a fixed 2 date membership test (that trick only ever worked because
    there were exactly two valid dates)."""
    now = now or datetime.datetime.now()
    today = today or datetime.date.today()
    window_end = today + datetime.timedelta(days=days_ahead)
    out = []
    for card in cards:
        car = parse_card(card, now=now)
        if car is None:
            continue
        if car.auction_ends_at:
            end_date = datetime.datetime.fromisoformat(car.auction_ends_at).date()
            if not (today <= end_date <= window_end):
                continue
        out.append(car)
    return out


def read_all_pages(page, today=None, days_ahead=7, max_pages=25):
    """Reads every page of the live Listings screen (the Live Auctions tab
    is the page's own default, never switched to Buy Now). Pagination
    clicks the real "next page" arrow, whose own aria-disabled attribute
    tells us cleanly when there is no more (found live, no next page
    existed at build time, so multi page clicking itself is unverified,
    the disabled check itself is proven). Stops early if a click ever
    returns the exact same set of listing hrefs as before, the same safety
    net Dealer Auction and DealerKit's own pagination already use."""
    seen_hrefs = set()
    out = []
    for _ in range(max_pages):
        cards = page.evaluate(_EXTRACT_JS)
        new_cards = [c for c in cards if c.get("href") not in seen_hrefs]
        if not new_cards and seen_hrefs:
            break
        seen_hrefs.update(c.get("href") for c in cards)
        out.extend(parse_listing(new_cards, today=today, days_ahead=days_ahead))
        next_btn = page.query_selector('div[aria-disabled="false"]')
        if not next_btn:
            break
        try:
            next_btn.click()
            page.wait_for_timeout(1200)
        except Exception:
            break
    return out


def read_live(playwright, today=None, days_ahead=7, headless=True):
    ctx = browser.open_reader_context(playwright, "dealerway", headless=headless)
    try:
        page = ctx.pages[0] if ctx.pages else ctx.new_page()
        page.goto(LIST_URL, wait_until="networkidle", timeout=45000)
        page.wait_for_timeout(1500)
        if not page.query_selector('a[href^="/listings/"]'):
            raise RuntimeError(
                "DealerWay listings did not render any vehicles. The page may "
                "have changed or the login may have expired. Run: "
                "python3 login.py dealerway manual")
        return read_all_pages(page, today=today, days_ahead=days_ahead)
    finally:
        ctx.close()


def _detail_fields(text):
    """Pure text parsing of a real detail page's own inner_text (so it can
    be tested without a browser, the same split already used for
    auction4cars.py's own _detail_fields), returning a plain dict: owners,
    service_history, no_v5c, mech_issue, bodywork_issue, other_notes. A
    field not found is left out of the dict entirely, never guessed, so a
    caller can tell "not on this page" apart from a real empty value."""
    lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
    out = {}

    def _after(label, offset=1):
        try:
            idx = lines.index(label)
        except ValueError:
            return None
        j = idx + offset
        return lines[j] if j < len(lines) else None

    owners = _after("Number of Owners")
    if owners and owners.isdigit():
        out["owners"] = int(owners)

    service = _after("Service Record")
    if service:
        out["service_history"] = service.strip().lower()

    if _after("Has V5C Logbook") == "No":
        out["no_v5c"] = True

    # Each disclosed issue is a fixed 3 line shape: the section heading,
    # "Disclosed By Seller", "Yes", then the real free text reason.
    try:
        mech_idx = lines.index("Electrical or Mechanical Issues")
        if mech_idx + 2 < len(lines) and lines[mech_idx + 1] == "Disclosed By Seller" \
                and lines[mech_idx + 2] == "Yes" and mech_idx + 3 < len(lines):
            out["mech_issue"] = lines[mech_idx + 3]
    except ValueError:
        pass

    try:
        body_idx = lines.index("Bodywork Issues")
        if body_idx + 1 < len(lines) and lines[body_idx + 1] != "No Bodywork Issues Reported":
            out["bodywork_issue"] = lines[body_idx + 1]
    except ValueError:
        pass

    try:
        other_idx = lines.index("Other")
        if other_idx + 2 < len(lines) and lines[other_idx + 1] == "Disclosed By Seller" \
                and lines[other_idx + 2] == "Yes" and other_idx + 3 < len(lines):
            out["other_notes"] = lines[other_idx + 3]
    except ValueError:
        pass

    return out


def enrich_from_detail(page, car):
    """Visits the car's own detail page for owners (the hard gate needs a
    real count, never on the list card) and the free text issue
    descriptions (real reasons, not just a red or green dot). Real fields
    confirmed live against a captured detail page (data/inspect/
    dealerway_detail.html): "Number of Owners", "Bodywork Issues",
    "Electrical or Mechanical Issues" each with "Disclosed By Seller" plus
    a free text line, and a separate "Other" disclosure. Never guessed:
    a field not found on the page is left on the car as it was, never
    invented."""
    page.goto(car.listing_url, wait_until="networkidle", timeout=45000)
    page.wait_for_timeout(1200)
    fields = _detail_fields(page.inner_text("body"))

    if "owners" in fields:
        car.owners = fields["owners"]
    if "service_history" in fields:
        car.service_history = fields["service_history"]
    if fields.get("no_v5c"):
        car.notes = (car.notes + " No V5C logbook on the listing.").strip()
    # A mech_issue already set from the list card's own red icon keeps its
    # meaning while gaining the real disclosed wording here.
    if "mech_issue" in fields:
        car.mech_issue = fields["mech_issue"]
    if "bodywork_issue" in fields:
        car.notes = (car.notes + " " + fields["bodywork_issue"]).strip()
    if "other_notes" in fields:
        car.notes = (car.notes + " " + fields["other_notes"]).strip()

    return car


def deep_read(page, listing_url, asked_at=None):
    """The shortlist deep read of one DealerWay car page (2026-09-17): the
    page's words (the same Vehicle Details, MOT and Service History blocks
    _detail_fields reads), handed to bidbrain.deep_read.from_dealerway."""
    from bidbrain import deep_read as _deep
    page.goto(listing_url, wait_until="networkidle", timeout=45000)
    if "/login" in (page.url or "").lower():
        return _deep.failed("DealerWay login needed. Run: python3 login.py dealerway manual", asked_at)
    page.wait_for_timeout(1200)
    d = _deep.from_dealerway(page.inner_text("body"))
    d["read_at"] = _deep.now_stamp()
    d["asked_at"] = asked_at
    return d
