package dev.nachlakes.hevybridge;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

public class HevyBridgeService extends AccessibilityService {
    private final Handler main = new Handler(Looper.getMainLooper());
    private LocalServer server;
    private volatile JSONObject lastState = new JSONObject();
    private volatile long lastEventMs = 0L;

    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        if (server == null) {
            server = new LocalServer(this, 18090);
            server.start();
        }
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        lastEventMs = System.currentTimeMillis();
        if (event != null && "com.hevy".contentEquals(String.valueOf(event.getPackageName()))) {
            try {
                lastState = extractStateInternal();
            } catch (Exception ignored) {}
        }
    }

    @Override
    public void onInterrupt() {}

    @Override
    public void onDestroy() {
        if (server != null) server.shutdown();
        super.onDestroy();
    }

    public JSONObject getStateJson() throws Exception {
        return onMain(() -> {
            JSONObject state = extractStateInternal();
            lastState = state;
            return state;
        });
    }

    public JSONObject handleAction(String action, int delta) throws Exception {
        return onMain(() -> {
            boolean ok = false;
            String detail = "no-op";
            if ("set_done".equals(action)) {
                ok = clickSetDone();
                detail = ok ? "clicked set done" : "set done target not found";
            } else if ("skip_rest".equals(action)) {
                ok = clickTextContains("skip");
                detail = ok ? "clicked skip" : "skip target not found";
            } else if ("timer_adjust".equals(action)) {
                String target = delta >= 0 ? "+15" : "-15";
                ok = clickTextContains(target);
                detail = ok ? "clicked " + target : target + " target not found";
            }
            JSONObject out = new JSONObject();
            out.put("ok", ok);
            out.put("action", action);
            out.put("detail", detail);
            out.put("state", extractStateInternal());
            return out;
        });
    }

    public JSONObject dumpWindowJson() throws Exception {
        return onMain(() -> {
            AccessibilityNodeInfo root = getRootInActiveWindow();
            JSONObject out = new JSONObject();
            out.put("ok", root != null);
            out.put("tree", root == null ? JSONObject.NULL : nodeToJson(root, 0));
            if (root != null) root.recycle();
            return out;
        });
    }

    private JSONObject extractStateInternal() throws Exception {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        JSONObject out = new JSONObject();
        out.put("source", "phone-bridge-accessibility");
        out.put("timestamp", System.currentTimeMillis() / 1000.0);
        out.put("lastEventMs", lastEventMs);
        if (root == null) {
            out.put("phase", "idle");
            out.put("exercise", "Open Hevy");
            out.put("setCurrent", 0);
            out.put("setTotal", 0);
            out.put("weight", "");
            out.put("reps", "");
            out.put("restSeconds", 0);
            out.put("rawTexts", new JSONArray());
            return out;
        }

        List<String> texts = new ArrayList<>();
        collectTexts(root, texts, new HashSet<>());
        root.recycle();

        JSONArray raw = new JSONArray();
        for (String t : texts) raw.put(t);
        out.put("rawTexts", raw);

        boolean inWorkoutLogger = containsExact(texts, "Log Workout") || containsLower(texts, "rest timer") || containsExact(texts, "+ Add Set");
        if (!inWorkoutLogger) {
            out.put("phase", "idle");
            out.put("exercise", "Open active Hevy workout");
            out.put("setCurrent", 0);
            out.put("setTotal", 0);
            out.put("weight", "");
            out.put("reps", "");
            out.put("restSeconds", 0);
            return out;
        }

        boolean hasSkip = containsLower(texts, "skip");
        Integer timer = findTimerSeconds(texts);
        String phase = hasSkip && timer != null ? "rest" : "exercise";
        String exercise = guessExercise(texts);
        int[] sets = guessSets(texts);
        String weight = guessAfterHeader(texts, "KG", "WEIGHT");
        String reps = guessAfterHeader(texts, "REPS", "REPETITIONS");

        out.put("phase", phase);
        out.put("exercise", exercise);
        out.put("setCurrent", sets[0]);
        out.put("setTotal", sets[1]);
        out.put("weight", weight);
        out.put("reps", reps);
        out.put("restSeconds", timer == null ? 0 : timer);
        return out;
    }

    private void collectTexts(AccessibilityNodeInfo node, List<String> out, Set<Integer> seen) {
        if (node == null) return;
        int id = System.identityHashCode(node);
        if (seen.contains(id)) return;
        seen.add(id);
        CharSequence text = node.getText();
        CharSequence desc = node.getContentDescription();
        addText(out, text);
        addText(out, desc);
        for (int i = 0; i < node.getChildCount(); i++) {
            AccessibilityNodeInfo child = node.getChild(i);
            if (child != null) {
                collectTexts(child, out, seen);
                child.recycle();
            }
        }
    }

    private void addText(List<String> out, CharSequence cs) {
        if (cs == null) return;
        String s = cs.toString().trim();
        if (s.length() > 0 && !out.contains(s)) out.add(s);
    }

    private boolean containsExact(List<String> texts, String needle) {
        for (String t : texts) if (t.trim().equalsIgnoreCase(needle)) return true;
        return false;
    }

    private boolean containsLower(List<String> texts, String needle) {
        for (String t : texts) if (t.toLowerCase().contains(needle.toLowerCase())) return true;
        return false;
    }

    private Integer findTimerSeconds(List<String> texts) {
        for (String t : texts) {
            String s = t.trim();
            if (s.matches("\\d{1,2}:\\d{2}")) {
                String[] p = s.split(":");
                return Integer.parseInt(p[0]) * 60 + Integer.parseInt(p[1]);
            }
        }
        return null;
    }

    private String guessExercise(List<String> texts) {
        Set<String> stop = new HashSet<>();
        String[] stops = {"Log Workout", "Duration", "Volume", "Sets", "Finish", "Add notes here...", "Rest Timer", "SET", "PREVIOUS", "KG", "REPS", "TIME", "+ Add Set", "+ Add Exercise", "Settings", "Discard Workout", "Skip", "+15", "-15"};
        for (String s : stops) stop.add(s.toLowerCase());
        for (String t : texts) {
            String x = t.trim();
            if (x.length() < 3) continue;
            if (stop.contains(x.toLowerCase())) continue;
            if (x.matches("\\d+")) continue;
            if (x.matches("\\d{1,2}:\\d{2}")) continue;
            if (x.toLowerCase().contains("min") || x.toLowerCase().contains("kg")) continue;
            return x;
        }
        return "Hevy";
    }

    private int[] guessSets(List<String> texts) {
        int current = 0;
        int total = 0;
        for (int i = 0; i < texts.size(); i++) {
            if ("SET".equalsIgnoreCase(texts.get(i).trim()) && i + 1 < texts.size()) {
                try { current = Integer.parseInt(texts.get(i + 1).trim()); } catch (Exception ignored) {}
            }
        }
        if (current == 0) {
            for (String t : texts) {
                if (t.matches("\\d+")) { try { current = Integer.parseInt(t); break; } catch (Exception ignored) {} }
            }
        }
        total = Math.max(current, 1);
        return new int[]{current, total};
    }

    private String guessAfterHeader(List<String> texts, String... headers) {
        for (int i = 0; i < texts.size() - 1; i++) {
            for (String h : headers) {
                if (h.equalsIgnoreCase(texts.get(i).trim())) return texts.get(i + 1).trim();
            }
        }
        return "";
    }

    private boolean clickSetDone() {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) return false;
        try {
            AccessibilityNodeInfo target = findLikelySetDone(root);
            if (target != null) {
                boolean ok = clickNode(target);
                target.recycle();
                return ok;
            }
            return clickRightmostClickable(root);
        } finally {
            root.recycle();
        }
    }

    private AccessibilityNodeInfo findLikelySetDone(AccessibilityNodeInfo node) {
        if (node == null) return null;
        CharSequence desc = node.getContentDescription();
        String d = desc == null ? "" : desc.toString().toLowerCase();
        CharSequence text = node.getText();
        String t = text == null ? "" : text.toString().toLowerCase();
        String cls = node.getClassName() == null ? "" : node.getClassName().toString().toLowerCase();
        if ((d.contains("done") || d.contains("complete") || t.contains("done") || cls.contains("checkbox")) && node.isEnabled()) {
            return AccessibilityNodeInfo.obtain(node);
        }
        for (int i = 0; i < node.getChildCount(); i++) {
            AccessibilityNodeInfo child = node.getChild(i);
            AccessibilityNodeInfo found = findLikelySetDone(child);
            if (child != null) child.recycle();
            if (found != null) return found;
        }
        return null;
    }

    private boolean clickTextContains(String needle) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) return false;
        try {
            AccessibilityNodeInfo target = findByText(root, needle.toLowerCase());
            if (target == null) return false;
            boolean ok = clickNode(target);
            target.recycle();
            return ok;
        } finally {
            root.recycle();
        }
    }

    private AccessibilityNodeInfo findByText(AccessibilityNodeInfo node, String needle) {
        if (node == null) return null;
        String text = node.getText() == null ? "" : node.getText().toString().toLowerCase();
        String desc = node.getContentDescription() == null ? "" : node.getContentDescription().toString().toLowerCase();
        if ((text.contains(needle) || desc.contains(needle)) && node.isEnabled()) return AccessibilityNodeInfo.obtain(node);
        for (int i = 0; i < node.getChildCount(); i++) {
            AccessibilityNodeInfo child = node.getChild(i);
            AccessibilityNodeInfo found = findByText(child, needle);
            if (child != null) child.recycle();
            if (found != null) return found;
        }
        return null;
    }

    private boolean clickNode(AccessibilityNodeInfo node) {
        if (node == null) return false;
        AccessibilityNodeInfo n = AccessibilityNodeInfo.obtain(node);
        while (n != null) {
            if (n.isClickable() && n.isEnabled()) {
                boolean ok = n.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                n.recycle();
                if (ok) return true;
                break;
            }
            AccessibilityNodeInfo parent = n.getParent();
            n.recycle();
            n = parent;
        }
        Rect r = new Rect();
        node.getBoundsInScreen(r);
        if (!r.isEmpty()) return gestureTap(r.centerX(), r.centerY());
        return false;
    }

    private boolean clickRightmostClickable(AccessibilityNodeInfo root) {
        List<AccessibilityNodeInfo> nodes = new ArrayList<>();
        collectClickable(root, nodes);
        AccessibilityNodeInfo best = null;
        int bestX = -1;
        for (AccessibilityNodeInfo n : nodes) {
            Rect r = new Rect();
            n.getBoundsInScreen(r);
            if (r.centerX() > bestX && r.centerY() > 300) { bestX = r.centerX(); best = n; }
        }
        boolean ok = best != null && clickNode(best);
        for (AccessibilityNodeInfo n : nodes) n.recycle();
        return ok;
    }

    private void collectClickable(AccessibilityNodeInfo node, List<AccessibilityNodeInfo> out) {
        if (node == null) return;
        if (node.isClickable() && node.isEnabled()) out.add(AccessibilityNodeInfo.obtain(node));
        for (int i = 0; i < node.getChildCount(); i++) {
            AccessibilityNodeInfo child = node.getChild(i);
            if (child != null) {
                collectClickable(child, out);
                child.recycle();
            }
        }
    }

    private boolean gestureTap(int x, int y) {
        Path path = new Path();
        path.moveTo(x, y);
        GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(path, 0, 80);
        GestureDescription gesture = new GestureDescription.Builder().addStroke(stroke).build();
        return dispatchGesture(gesture, null, null);
    }

    private JSONObject nodeToJson(AccessibilityNodeInfo n, int depth) throws Exception {
        JSONObject o = new JSONObject();
        o.put("text", n.getText() == null ? "" : n.getText().toString());
        o.put("desc", n.getContentDescription() == null ? "" : n.getContentDescription().toString());
        o.put("class", n.getClassName() == null ? "" : n.getClassName().toString());
        o.put("clickable", n.isClickable());
        Rect r = new Rect(); n.getBoundsInScreen(r);
        o.put("bounds", r.flattenToString());
        if (depth < 8) {
            JSONArray children = new JSONArray();
            for (int i = 0; i < n.getChildCount(); i++) {
                AccessibilityNodeInfo c = n.getChild(i);
                if (c != null) { children.put(nodeToJson(c, depth + 1)); c.recycle(); }
            }
            o.put("children", children);
        }
        return o;
    }

    private <T> T onMain(Callable<T> c) throws Exception {
        if (Looper.myLooper() == Looper.getMainLooper()) return c.call();
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<T> result = new AtomicReference<>();
        AtomicReference<Exception> error = new AtomicReference<>();
        main.post(() -> {
            try { result.set(c.call()); } catch (Exception e) { error.set(e); } finally { latch.countDown(); }
        });
        latch.await();
        if (error.get() != null) throw error.get();
        return result.get();
    }

    static class LocalServer extends Thread {
        private final HevyBridgeService svc;
        private final int port;
        private volatile boolean running = true;
        private ServerSocket server;

        LocalServer(HevyBridgeService svc, int port) { this.svc = svc; this.port = port; setName("HevyBridgeHttp"); }

        public void shutdown() {
            running = false;
            try { if (server != null) server.close(); } catch (Exception ignored) {}
        }

        @Override public void run() {
            try {
                server = new ServerSocket(port, 50, InetAddress.getByName("127.0.0.1"));
                while (running) handle(server.accept());
            } catch (Exception ignored) {}
        }

        private void handle(Socket socket) {
            new Thread(() -> {
                try (Socket s = socket) {
                    BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
                    String request = br.readLine();
                    if (request == null) return;
                    String[] parts = request.split(" ");
                    String method = parts.length > 0 ? parts[0] : "GET";
                    String path = parts.length > 1 ? parts[1] : "/";
                    int contentLength = 0;
                    String line;
                    while ((line = br.readLine()) != null && line.length() > 0) {
                        if (line.toLowerCase().startsWith("content-length:")) contentLength = Integer.parseInt(line.substring(15).trim());
                    }
                    char[] bodyChars = new char[contentLength];
                    if (contentLength > 0) br.read(bodyChars);
                    String body = new String(bodyChars);
                    JSONObject response;
                    if (path.startsWith("/health")) {
                        response = new JSONObject().put("ok", true).put("service", "hevy-bridge").put("port", port);
                    } else if (path.startsWith("/api/hevy/live") || path.startsWith("/state")) {
                        response = svc.getStateJson();
                    } else if (path.startsWith("/api/hevy/window") || path.startsWith("/window")) {
                        response = svc.dumpWindowJson();
                    } else if (path.startsWith("/api/hevy/action") && "POST".equals(method)) {
                        JSONObject in = body.length() > 0 ? new JSONObject(body) : new JSONObject();
                        response = svc.handleAction(in.optString("action"), in.optInt("delta", 0));
                    } else {
                        response = new JSONObject().put("ok", false).put("error", "not_found").put("path", path);
                    }
                    writeJson(s.getOutputStream(), 200, response.toString());
                } catch (Exception e) {
                    try { writeJson(socket.getOutputStream(), 500, new JSONObject().put("ok", false).put("error", e.toString()).toString()); } catch (Exception ignored) {}
                }
            }, "HevyBridgeHttpClient").start();
        }

        private void writeJson(OutputStream os, int code, String body) throws Exception {
            byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
            String headers = "HTTP/1.1 " + code + " OK\r\n" +
                    "Content-Type: application/json\r\n" +
                    "Access-Control-Allow-Origin: *\r\n" +
                    "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" +
                    "Access-Control-Allow-Headers: Content-Type\r\n" +
                    "Content-Length: " + bytes.length + "\r\n\r\n";
            os.write(headers.getBytes(StandardCharsets.UTF_8));
            os.write(bytes);
            os.flush();
        }
    }
}
