package com.unvired.dbplugin;

import android.content.Context;
import android.content.SharedPreferences;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.util.Log;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.UUID;

public class DBPlugin extends CordovaPlugin {
    private static final String TAG = "DBPlugin";
    private static final String DB_TYPE_APP = "AppDb";
    private static final String DB_TYPE_FRAMEWORK = "FrameworkDb";

    private SQLiteDatabase appDb;
    private SQLiteDatabase fwDb;
    private String appDbPath;
    private String fwDbPath;

    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        try {
            switch (action) {
                case "create":
                    create(args, callbackContext);
                    return true;
                case "execute":
                    execute(args, callbackContext);
                    return true;
                case "close":
                    close(callbackContext);
                    return true;
                case "getDBFilePath":
                    getDBFilePath(args, callbackContext);
                    return true;
                case "getUserDirectoryPath":
                    getUserDirectoryPath(args, callbackContext);
                    return true;
                case "saveWebDB":
                    saveWebDB(args, callbackContext);
                    return true;
                case "exportWebDB":
                    exportWebDB(args, callbackContext);
                    return true;
                case "deleteUserData":
                    deleteUserData(args, callbackContext);
                    return true;
                case "getEncryptionKey":
                    getEncryptionKey(callbackContext);
                    return true;
                default:
                    callbackContext.error("Invalid action: " + action);
                    return false;
            }
        } catch (Exception e) {
            Log.e(TAG, "Error executing action: " + action, e);
            callbackContext.error("Error: " + e.getMessage());
            return false;
        }
    }

    /**
     * Checks if the database file at the given path is plaintext (unencrypted).
     * Attempts to open the database without a key and run a simple query.
     * @param dbPath Path to the database file.
     * @return true if the database is plaintext, false if encrypted or unreadable.
     */
    private boolean isPlaintextDatabase(String dbPath) {
        File f = new File(dbPath);
        if (!f.exists()) return false;
        SQLiteDatabase testDb = null;
        try {
            testDb = SQLiteDatabase.openOrCreateDatabase(dbPath, "", null, null, null);
            testDb.rawQuery("SELECT count(*) FROM sqlite_master;", null).close();
            return true;
        } catch (Exception e) {
            return false;
        } finally {
            if (testDb != null) { try { testDb.close(); } catch (Exception ignored) {} }
        }
    }

    /**
     * Opens (or creates) a database with optional encryption.
     * @param dbPath       Path to the database file.
     * @param encryptionKey Encryption key (empty string = no encryption).
     * @return Opened SQLiteDatabase instance.
     */
    private SQLiteDatabase openDatabase(String dbPath, String encryptionKey) throws Exception {
        if (!encryptionKey.isEmpty() && isPlaintextDatabase(dbPath)) {
            // Existing DB is not encrypted, ignore the key.
            encryptionKey = "";
        }
        return SQLiteDatabase.openOrCreateDatabase(dbPath, encryptionKey, null, null, null);
    }

    private void create(JSONArray args, CallbackContext callbackContext) throws JSONException {
        JSONObject params = args.getJSONObject(0);
        String userId = params.getString("userId");
        // optString returns "" when key is missing or JSON null — no encryption if not provided
        String encryptionKey = params.optString("encryptionKey", "");

        if (userId == null || userId.isEmpty()) {
            callbackContext.error("userId is required");
            return;
        }

        String userPath = getUserDirectoryPath(userId);
        if (userPath == null) {
            callbackContext.error("Failed to create user directory");
            return;
        }

        try {
            System.loadLibrary("sqlcipher");

            // Open (and migrate if needed) framework database
            fwDbPath = userPath + "/framework.db";
            fwDb = openDatabase(fwDbPath, encryptionKey);
            fwDb.enableWriteAheadLogging();
            fwDb.execSQL("PRAGMA read_uncommitted = 1;");
            fwDb.execSQL("PRAGMA foreign_keys = ON;");

            // Open (and migrate if needed) app database
            appDbPath = userPath + "/app.db";
            appDb = openDatabase(appDbPath, encryptionKey);
            appDb.enableWriteAheadLogging();
            appDb.execSQL("PRAGMA read_uncommitted = 1;");
            appDb.execSQL("PRAGMA foreign_keys = ON;");

            callbackContext.success("Databases created successfully");
        } catch (Exception e) {
            Log.e(TAG, "Error creating databases", e);
            callbackContext.error("Error creating databases: " + e.getMessage());
        }
    }

    private void execute(JSONArray args, CallbackContext callbackContext) throws JSONException {
        JSONObject params = args.getJSONObject(0);
        String userId = params.getString("userId");
        String dbType = params.getString("dbType");
        String query = params.getString("query");
        JSONArray queryArgs = params.optJSONArray("args");

        if (userId == null || userId.isEmpty()) {
            callbackContext.error("userId is required");
            return;
        }

        if (!DB_TYPE_APP.equals(dbType) && !DB_TYPE_FRAMEWORK.equals(dbType)) {
            callbackContext.error("Invalid dbType");
            return;
        }

        if (query == null || query.isEmpty()) {
            callbackContext.error("query is required");
            return;
        }

        SQLiteDatabase db = DB_TYPE_APP.equals(dbType) ? appDb : fwDb;
        if (db == null) {
            try {
                create(args, callbackContext);
                db = DB_TYPE_APP.equals(dbType) ? appDb : fwDb;
            } catch (Exception e) {
                callbackContext.error("Error creating database: " + e.getMessage());
                return;
            }
        }

        try {
            Object[] selectionArgs = null;
            if (queryArgs != null && queryArgs.length() > 0) {
                selectionArgs = new Object[queryArgs.length()];
                for (int i = 0; i < queryArgs.length(); i++) {
                    Object val = queryArgs.get(i);
                    selectionArgs[i] = (val == null || val == JSONObject.NULL) ? null : val;
                }
            }

            JSONArray results = new JSONArray();
            android.database.Cursor cursor = db.rawQuery(query, selectionArgs);
            if (cursor != null) {
                try {
                    String[] columnNames = cursor.getColumnNames();
                    while (cursor.moveToNext()) {
                        JSONObject row = new JSONObject();
                        for (String columnName : columnNames) {
                            int columnIndex = cursor.getColumnIndex(columnName);
                            switch (cursor.getType(columnIndex)) {
                                case android.database.Cursor.FIELD_TYPE_INTEGER:
                                    row.put(columnName, cursor.getLong(columnIndex));
                                    break;
                                case android.database.Cursor.FIELD_TYPE_FLOAT:
                                    row.put(columnName, cursor.getDouble(columnIndex));
                                    break;
                                case android.database.Cursor.FIELD_TYPE_STRING:
                                    row.put(columnName, cursor.getString(columnIndex));
                                    break;
                                case android.database.Cursor.FIELD_TYPE_BLOB:
                                    row.put(columnName, cursor.getBlob(columnIndex));
                                    break;
                                default:
                                    row.put(columnName, JSONObject.NULL);
                            }
                        }
                        results.put(row);
                    }
                } finally {
                    cursor.close();
                }
            }
            callbackContext.success(results);
        } catch (SQLiteException e) {
            Log.e(TAG, "Error executing query", e);
            callbackContext.error("Error executing query: " + e.getMessage());
        }
    }

    private void close(CallbackContext callbackContext) {
        try {
            if (appDb != null) {
                appDb.close();
                appDb = null;
            }
            if (fwDb != null) {
                fwDb.close();
                fwDb = null;
            }
            callbackContext.success("DB connection closed");
        } catch (SQLiteException e) {
            Log.e(TAG, "Error closing databases", e);
            callbackContext.error("Error closing databases: " + e.getMessage());
        }
    }

    private void getDBFilePath(JSONArray args, CallbackContext callbackContext) throws JSONException {
        JSONObject params = args.getJSONObject(0);
        String dbType = params.getString("dbType");

        String path = null;
        if (DB_TYPE_APP.equals(dbType)) {
            path = appDbPath;
        } else if (DB_TYPE_FRAMEWORK.equals(dbType)) {
            path = fwDbPath;
        }

        if (path != null) {
            callbackContext.success(path);
        } else {
            callbackContext.error("Invalid dbType or database not initialized");
        }
    }

    private void getUserDirectoryPath(JSONArray args, CallbackContext callbackContext) throws JSONException {
        JSONObject params = args.getJSONObject(0);
        String userId = params.getString("userId");

        if (userId == null || userId.isEmpty()) {
            callbackContext.error("userId is required");
            return;
        }

        String path = getUserDirectoryPath(userId);
        if (path != null) {
            callbackContext.success(path);
        } else {
            callbackContext.error("Failed to get user directory path");
        }
    }

    private String getUserDirectoryPath(String userId) {
        Context context = cordova.getActivity().getApplicationContext();
        File userDir = new File(context.getFilesDir(), userId);
        if (!userDir.exists()) {
            if (!userDir.mkdirs()) {
                Log.e(TAG, "Failed to create user directory: " + userDir.getAbsolutePath());
                return null;
            }
        }
        return userDir.getAbsolutePath();
    }

    private void saveWebDB(JSONArray args, CallbackContext callbackContext) throws JSONException {
        callbackContext.success("Web DB save is only for browser.");
    }

    private void exportWebDB(JSONArray args, CallbackContext callbackContext) throws JSONException {
        callbackContext.success();
    }
    
    private void deleteUserData(JSONArray args, CallbackContext callbackContext) throws JSONException {
        JSONObject params = args.getJSONObject(0);
        String userId = params.getString("userId");

        if (userId == null || userId.isEmpty()) {
            callbackContext.error("userId is required");
            return;
        }

        String userPath = getUserDirectoryPath(userId);
        if (userPath == null) {
            callbackContext.error("Failed to get user directory path");
            return;
        }

        File userDir = new File(userPath);
        if (!userDir.exists()) {
            callbackContext.error("User directory does not exist: " + userPath);
            return;
        }

        // Close databases if open and set to null
        if (appDb != null) {
            try {
                appDb.close();
            } catch (SQLiteException e) {
                Log.e(TAG, "Error closing appDb during user data deletion", e);
            }
            appDb = null;
        }
        if (fwDb != null) {
            try {
                fwDb.close();
            } catch (SQLiteException e) {
                Log.e(TAG, "Error closing fwDb during user data deletion", e);
            }
            fwDb = null;
        }

        boolean deleted = deleteDirectoryRecursively(userDir);
        if (deleted) {
            callbackContext.success("User directory deleted: " + userPath);
        } else {
            callbackContext.error("Failed to delete user directory: " + userPath);
        }
    }

    private boolean deleteDirectoryRecursively(File dir) {
        if (dir.isDirectory()) {
            File[] children = dir.listFiles();
            if (children != null) {
                for (File child : children) {
                    if (!deleteDirectoryRecursively(child)) {
                        return false;
                    }
                }
            }
        }
        return dir.delete();
    }

    private void getEncryptionKey(CallbackContext callbackContext) {
        cordova.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Context context = cordova.getActivity().getApplicationContext();
                    SharedPreferences prefs = context.getSharedPreferences("UnviredDBPrefs", Context.MODE_PRIVATE);
                    String key = prefs.getString("unvired_db_encryption_key", null);
                    if (key == null) {
                        key = UUID.randomUUID().toString();
                        SharedPreferences.Editor editor = prefs.edit();
                        editor.putString("unvired_db_encryption_key", key);
                        editor.apply();
                    }
                    callbackContext.success(key);
                } catch (Exception e) {
                    Log.e(TAG, "Error getting encryption key", e);
                    callbackContext.error("Error getting encryption key: " + e.getMessage());
                }
            }
        });
    }
}
