#!/usr/bin/env python3
"""
46 CARS INC. - Turbo AutoTrader Sender v2

Two deliberately separate transports:

1) SEND EXACT POWERSHELL
   Executes the pasted PowerShell request itself and changes ONLY the -Body
   argument to the JSON currently in the Payload box.
   No cookie/header reconstruction is performed.

2) SEND VIA SOCKS
   Reconstructs the visible HTTP request in Python so a SOCKS5/SOCKS5H proxy
   can be used. This mode is NOT byte/network-stack identical to PowerShell.

The app does not attempt to bypass CAPTCHA, WAF, access controls, or expired
sessions. If the site rejects a session, capture a fresh request from your own
browser session.
"""

from __future__ import annotations

import base64
import json
import os
import re
import shutil
import subprocess
import tempfile
import threading
import traceback
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from urllib.parse import quote, urlparse

import tkinter as tk
from tkinter import ttk, messagebox


ALLOWED_HOSTS = {"autotrader.ca", "www.autotrader.ca"}


@dataclass
class ParsedCookie:
    name: str
    value: str
    path: str = "/"
    domain: str = ""


@dataclass
class ParsedRequest:
    method: str = "POST"
    url: str = ""
    cookies: List[ParsedCookie] = field(default_factory=list)
    headers: Dict[str, str] = field(default_factory=dict)
    header_items: List[Tuple[str, str]] = field(default_factory=list)
    user_agent: str = ""
    content_type: str = "application/json"
    body: str = ""


def ps_unescape(value: str) -> str:
    sentinel = "\x00BT\x00"
    value = value.replace("``", sentinel)
    for a, b in [
        ('`"', '"'), ("`'", "'"), ("`$", "$"),
        ("`n", "\n"), ("`r", "\r"), ("`t", "\t"),
    ]:
        value = value.replace(a, b)
    return value.replace(sentinel, "`")


def quoted_switch(text: str, name: str) -> Optional[str]:
    m = re.search(
        rf'-{re.escape(name)}\s+"((?:`.|[^"])*)"',
        text, re.I | re.S
    )
    return ps_unescape(m.group(1)) if m else None


def parse_request(text: str) -> ParsedRequest:
    if not text.strip():
        raise ValueError("PowerShell request is empty.")

    p = ParsedRequest()

    p.url = (quoted_switch(text, "Uri") or "").strip()
    if not p.url:
        raise ValueError("Could not find -Uri.")

    mm = re.search(r'-Method\s+(?:"([^"]+)"|([A-Za-z]+))', text, re.I)
    if mm:
        p.method = (mm.group(1) or mm.group(2)).upper()

    p.user_agent = quoted_switch(text, "UserAgent") or ""
    p.content_type = quoted_switch(text, "ContentType") or "application/json"
    p.body = quoted_switch(text, "Body") or ""

    cp = re.compile(
        r'New-Object\s+System\.Net\.Cookie\(\s*'
        r'"((?:`.|[^"])*)"\s*,\s*'
        r'"((?:`.|[^"])*)"\s*,\s*'
        r'"((?:`.|[^"])*)"\s*,\s*'
        r'"((?:`.|[^"])*)"\s*\)',
        re.I,
    )
    for m in cp.finditer(text):
        p.cookies.append(ParsedCookie(
            ps_unescape(m.group(1)),
            ps_unescape(m.group(2)),
            ps_unescape(m.group(3)),
            ps_unescape(m.group(4)),
        ))

    hm = re.search(r'-Headers\s*@\{(.*?)\}\s*`?', text, re.I | re.S)
    if hm:
        hp = re.compile(
            r'"((?:`.|[^"])*)"\s*=\s*"((?:`.|[^"])*)"', re.S
        )
        for m in hp.finditer(hm.group(1)):
            k, v = ps_unescape(m.group(1)), ps_unescape(m.group(2))
            p.header_items.append((k, v))
            p.headers[k] = v

    return p


def validate_url(url: str) -> None:
    u = urlparse(url)
    host = (u.hostname or "").lower()
    if u.scheme.lower() != "https":
        raise ValueError("Only HTTPS requests are allowed.")
    if host not in ALLOWED_HOSTS and not host.endswith(".autotrader.ca"):
        raise ValueError(f"Unexpected destination host: {host}")


def validate_payload(text: str) -> str:
    if not text.strip():
        raise ValueError("Payload is empty.")
    try:
        json.loads(text)
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON payload: {e}") from e
    # Preserve the user's payload text instead of re-serializing it in Exact mode.
    return text.strip()


BODY_PATTERN = re.compile(
    r'-Body\s+"((?:`.|[^"])*)"',
    re.I | re.S
)


def build_exact_script(original_ps: str, payload: str) -> str:
    """
    Preserve the pasted script and replace only the -Body expression.
    Payload is passed via Base64 to avoid PowerShell quoting/injection issues.
    """
    if not BODY_PATTERN.search(original_ps):
        raise ValueError("Could not find a quoted -Body argument in PowerShell.")

    payload_b64 = base64.b64encode(payload.encode("utf-8")).decode("ascii")
    prefix = (
        "$__turboBody = [System.Text.Encoding]::UTF8.GetString("
        "[System.Convert]::FromBase64String("
        f"'{payload_b64}'"
        "))\n"
    )

    modified, n = BODY_PATTERN.subn("-Body $__turboBody", original_ps, count=1)
    if n != 1:
        raise ValueError("Could not replace the PowerShell -Body safely.")

    return prefix + modified


def exactness_report(original_ps: str, generated_script: str) -> str:
    """
    Verify that after removing our payload-variable prefix and restoring the
    Body placeholder, the request text matches the original outside -Body.
    """
    # Remove prefix line.
    stripped = generated_script.split("\n", 1)[1] if "\n" in generated_script else generated_script

    orig_no_body = BODY_PATTERN.sub("-Body <PAYLOAD>", original_ps, count=1)
    gen_no_body = re.sub(r'-Body\s+\$__turboBody', "-Body <PAYLOAD>", stripped, count=1, flags=re.I)

    same = (orig_no_body == gen_no_body)
    return (
        "EXACT POWERSHELL CHECK\n"
        f"All original request text outside -Body preserved: {'YES' if same else 'NO'}\n"
        "Transport in this mode: powershell.exe / Invoke-WebRequest\n"
        "Cookies remain in the original WebRequestSession CookieContainer.\n"
        "Headers/User-Agent/ContentType remain exactly as pasted.\n"
        "Only the value supplied to -Body is replaced.\n"
    )


def find_windows_powershell() -> str:
    candidates = [
        shutil.which("powershell.exe"),
        shutil.which("powershell"),
    ]
    for c in candidates:
        if c:
            return c
    raise RuntimeError(
        "Windows PowerShell was not found. This Exact mode is intended for Windows "
        "and requires powershell.exe."
    )


def run_exact_powershell(original_ps: str, payload: str, timeout: int = 90):
    script = build_exact_script(original_ps, payload)
    exe = find_windows_powershell()

    fd, path = tempfile.mkstemp(prefix="turbo_exact_", suffix=".ps1", text=True)
    os.close(fd)

    try:
        with open(path, "w", encoding="utf-8-sig", newline="\r\n") as f:
            f.write(script)

        creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
        result = subprocess.run(
            [
                exe,
                "-NoLogo",
                "-NoProfile",
                "-NonInteractive",
                "-ExecutionPolicy", "Bypass",
                "-File", path,
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            errors="replace",
            timeout=timeout,
            creationflags=creationflags,
        )
        return result, script
    finally:
        try:
            os.remove(path)
        except OSError:
            pass


def normalize_proxy(text: str) -> Optional[str]:
    raw = (text or "").strip()
    if not raw:
        return None

    raw = raw.replace(r"\:", ":")

    if raw.startswith(("socks5://", "socks5h://")):
        return raw

    parts = raw.split(":", 3)
    if len(parts) == 2:
        host, port = parts
        if not host or not port.isdigit():
            raise ValueError("Invalid HOST:PORT proxy.")
        return f"socks5h://{host}:{port}"

    if len(parts) == 4:
        host, port, user, password = parts
        if not host or not port.isdigit():
            raise ValueError("Invalid proxy host/port.")
        return (
            f"socks5h://{quote(user, safe='')}:{quote(password, safe='')}"
            f"@{host}:{port}"
        )

    raise ValueError(
        "SOCKS format: HOST:PORT, HOST:PORT:USER:PASSWORD, "
        "HOST:PORT\\:USER\\:PASSWORD, socks5://..., or socks5h://..."
    )


def reconstructed_headers(p: ParsedRequest) -> Dict[str, str]:
    # Preserve pasted application-level headers. Do not add browser impersonation headers.
    h = {}
    for k, v in p.header_items:
        h[k] = v

    if p.user_agent:
        h["User-Agent"] = p.user_agent
    if p.content_type:
        h["Content-Type"] = p.content_type

    if p.cookies:
        h["Cookie"] = "; ".join(f"{c.name}={c.value}" for c in p.cookies)

    # Transport must calculate these itself.
    for k in list(h):
        if k.lower() in {"content-length", "host", "connection"}:
            h.pop(k, None)

    return h


def send_via_socks(p: ParsedRequest, payload: str, proxy_text: str, timeout: int = 60):
    proxy = normalize_proxy(proxy_text)
    if not proxy:
        raise ValueError("SOCKS mode requires a proxy.")

    try:
        from curl_cffi import requests as crequests
    except ImportError as e:
        raise RuntimeError(
            "SOCKS mode requires curl_cffi.\n"
            "Install with:\npy -m pip install curl_cffi"
        ) from e

    # Deliberately DO NOT use curl_cffi browser impersonation here. The goal is
    # to preserve the pasted visible headers rather than silently add a second
    # set of browser-generated headers.
    response = crequests.request(
        method=p.method,
        url=p.url,
        headers=reconstructed_headers(p),
        data=payload.encode("utf-8"),
        proxies={"http": proxy, "https": proxy},
        timeout=timeout,
        allow_redirects=False,
    )
    return response


def redact_proxy(proxy: str) -> str:
    if not proxy:
        return "None"
    try:
        u = urlparse(proxy)
        auth = f"{u.username}:***@" if u.username else ""
        return f"{u.scheme}://{auth}{u.hostname}:{u.port}"
    except Exception:
        return "<configured>"


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("46 CARS INC. - Turbo Sender v2")
        self.geometry("1240x850")
        self.minsize(1000, 680)
        self._build()

    def _build(self):
        outer = ttk.Frame(self, padding=8)
        outer.pack(fill="both", expand=True)

        top = ttk.Panedwindow(outer, orient="horizontal")
        top.pack(fill="both", expand=True)

        lf = ttk.Labelframe(top, text="Original PowerShell Request", padding=5)
        rf = ttk.Labelframe(top, text="Replacement JSON Payload", padding=5)
        top.add(lf, weight=1)
        top.add(rf, weight=1)

        self.ps = tk.Text(lf, wrap="none", undo=True)
        self.ps.pack(fill="both", expand=True)

        self.payload = tk.Text(rf, wrap="none", undo=True)
        self.payload.pack(fill="both", expand=True)

        row = ttk.Frame(outer)
        row.pack(fill="x", pady=(8, 5))

        ttk.Label(row, text="SOCKS:").pack(side="left")
        self.proxy = tk.StringVar()
        ttk.Entry(row, textvariable=self.proxy, width=42).pack(
            side="left", padx=(5, 10), fill="x", expand=True
        )

        self.verify_btn = ttk.Button(row, text="Verify Exactness", command=self.verify)
        self.verify_btn.pack(side="left", padx=3)

        self.exact_btn = ttk.Button(row, text="Send Exact PowerShell", command=self.send_exact)
        self.exact_btn.pack(side="left", padx=3)

        self.socks_btn = ttk.Button(row, text="Send via SOCKS", command=self.send_socks)
        self.socks_btn.pack(side="left", padx=3)

        ttk.Button(row, text="Clear Output", command=lambda: self.out.delete("1.0", "end")).pack(
            side="left", padx=3
        )

        self.status = tk.StringVar(
            value="Exact mode executes the pasted PowerShell itself. SOCKS mode is reconstructed."
        )
        ttk.Label(outer, textvariable=self.status).pack(fill="x")

        of = ttk.Labelframe(outer, text="Verification / Response", padding=5)
        of.pack(fill="both", expand=True, pady=(5, 0))
        self.out = tk.Text(of, wrap="none", height=20)
        self.out.pack(fill="both", expand=True)

    def snapshot(self):
        ps = self.ps.get("1.0", "end-1c")
        payload = validate_payload(self.payload.get("1.0", "end-1c"))
        p = parse_request(ps)
        validate_url(p.url)
        return ps, payload, p

    def write(self, text):
        self.out.delete("1.0", "end")
        self.out.insert("1.0", text)

    def lock(self, yes):
        state = "disabled" if yes else "normal"
        for b in (self.verify_btn, self.exact_btn, self.socks_btn):
            b.configure(state=state)

    def verify(self):
        try:
            ps, payload, p = self.snapshot()
            script = build_exact_script(ps, payload)
            report = exactness_report(ps, script)
            report += (
                f"\nMETHOD: {p.method}\n"
                f"URL: {p.url}\n"
                f"COOKIES IN ORIGINAL WEBSESSION: {len(p.cookies)}\n"
                f"PASTED HEADERS: {len(p.header_items)}\n"
                f"USER-AGENT: {p.user_agent}\n"
                f"CONTENT-TYPE: {p.content_type}\n"
                f"SOCKS FIELD: {redact_proxy(normalize_proxy(self.proxy.get()) or '')}\n"
            )
            self.write(report)
            self.status.set("Exactness verification complete.")
        except Exception as e:
            messagebox.showerror("Verification failed", str(e))

    def send_exact(self):
        try:
            ps, payload, p = self.snapshot()
            if self.proxy.get().strip():
                ok = messagebox.askyesno(
                    "SOCKS is not used in Exact mode",
                    "Exact PowerShell mode does NOT apply the SOCKS field.\n\n"
                    "That is intentional: applying SOCKS through a different HTTP "
                    "transport would stop this from being the original PowerShell request.\n\n"
                    "Send the exact PowerShell request without the SOCKS field?"
                )
                if not ok:
                    return
        except Exception as e:
            messagebox.showerror("Validation failed", str(e))
            return

        self.lock(True)
        self.status.set("Executing original PowerShell request with replacement body...")
        self.write("Running exact PowerShell request...\n")

        threading.Thread(
            target=self._exact_worker,
            args=(ps, payload),
            daemon=True,
        ).start()

    def _exact_worker(self, ps, payload):
        try:
            result, script = run_exact_powershell(ps, payload)
            check = exactness_report(ps, script)
            output = (
                check
                + "\nPROCESS EXIT CODE: " + str(result.returncode)
                + "\n\nSTDOUT:\n" + (result.stdout or "<empty>")
                + "\n\nSTDERR:\n" + (result.stderr or "<empty>")
            )
            status = f"Exact PowerShell completed, exit code {result.returncode}."
        except Exception as e:
            output = "EXACT REQUEST FAILED\n\n" + traceback.format_exc()
            status = f"Exact request failed: {e}"
        self.after(0, self._done, output, status)

    def send_socks(self):
        try:
            ps, payload, p = self.snapshot()
            proxy = normalize_proxy(self.proxy.get())
            if not proxy:
                raise ValueError("Enter a SOCKS proxy for SOCKS mode.")
        except Exception as e:
            messagebox.showerror("Validation failed", str(e))
            return

        ok = messagebox.askyesno(
            "SOCKS mode is not identical",
            "SOCKS mode preserves the visible pasted headers/cookies/User-Agent, "
            "but it uses libcurl instead of PowerShell/.NET and is therefore NOT "
            "an identical network request.\n\nContinue?"
        )
        if not ok:
            return

        self.lock(True)
        self.status.set("Sending reconstructed request through SOCKS...")
        self.write("Sending via SOCKS (non-identical transport)...\n")
        threading.Thread(
            target=self._socks_worker,
            args=(p, payload, self.proxy.get()),
            daemon=True,
        ).start()

    def _socks_worker(self, p, payload, proxy):
        try:
            r = send_via_socks(p, payload, proxy)
            try:
                body = json.dumps(r.json(), indent=2, ensure_ascii=False)
            except Exception:
                body = r.text

            output = (
                "TRANSPORT: curl_cffi via SOCKS — NOT identical to PowerShell\n"
                f"HTTP STATUS: {r.status_code}\n"
                f"URL: {p.url}\n"
                f"PROXY: {redact_proxy(normalize_proxy(proxy) or '')}\n\n"
                "RESPONSE HEADERS:\n"
                + json.dumps(dict(r.headers), indent=2, ensure_ascii=False)
                + "\n\nRESPONSE BODY:\n"
                + body
            )
            status = f"SOCKS request completed: HTTP {r.status_code}."
        except Exception as e:
            output = "SOCKS REQUEST FAILED\n\n" + traceback.format_exc()
            status = f"SOCKS request failed: {e}"
        self.after(0, self._done, output, status)

    def _done(self, output, status):
        self.write(output)
        self.status.set(status)
        self.lock(False)


if __name__ == "__main__":
    App().mainloop()
