package com.secureline.server;

import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.os.SystemProperties;
import android.util.Slog;

import com.android.secureline.ISecureLineVoice;
import com.android.server.SystemService;

import org.json.JSONObject;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;

public final class SecureLineVoiceService extends SystemService {

    private static final String TAG =
            "SecureLineVoiceService";

    private static final String DIR =
            "/data/system/secureline/";

    private static final String FILE =
            "voice_config.json";

    private static final String ALLOWED_PACKAGE =
            "com.secureline.voice";

    private static final float MIN_VALUE = 0.6f;
    private static final float MAX_VALUE = 1.8f;

    private static final float DEFAULT_VARIATION =
            0.0003f;

    private boolean mEnabled = false;

    private String mMode = "DEEP_VOICE";

    private float mPitch = 1.20f;
    private float mFormant = 0.93f;
    private float mVariation = 0.00020f;

    private float mWarmth = 0.16f;
    private float mCompression = 0.35f;
    private float mDrive = 0.08f;
    private float mOutputGain = 0.95f;
    private float mSpectralTilt = -0.10f;
    private float mPresence = 0.05f;

    private File mConfigFile;
    private LemenzoCallRedirectController mCallRedirectController;

    public SecureLineVoiceService(Context context) {
        super(context);
    }

    @Override
    public void onStart() {

        try {

            File dir = new File(DIR);

            if (!dir.exists()) {
                dir.mkdirs();

                dir.setReadable(false, false);
                dir.setWritable(true, true);
                dir.setExecutable(true, true);
            }

            mConfigFile =
                    new File(dir, FILE);

            loadConfig();

            applyToNative();

            mCallRedirectController = new LemenzoCallRedirectController(getContext());
            mCallRedirectController.start(mEnabled);

            publishBinderService(
                    "secureline_voice",
                    mBinder
            );

            Slog.i(TAG, "Voice service started");

        } catch (Throwable e) {

            Slog.e(
                    TAG,
                    "Voice service init failed",
                    e
            );
        }
    }

    private final IBinder mBinder =
            new ISecureLineVoice.Stub() {

        @Override
        public void setVoiceEnabled(boolean enabled) {

            enforceCaller();

            mEnabled = enabled;

            applyToNative();

            if (mCallRedirectController != null) {
                mCallRedirectController.setEnabled(enabled);
            }

            saveConfig();
        }

        @Override
        public boolean isVoiceEnabled() {

            enforceCaller();

            return mEnabled;
        }

        @Override
        public void setVoiceMode(String mode) {

            enforceCaller();

            if (mode == null) {
                return;
            }

            mMode =
                    mode.toUpperCase();

            applyMode(mMode);

            applyToNative();

            saveConfig();
        }

        @Override
        public String getVoiceMode() {

            enforceCaller();

            return mMode;
        }

        @Override
        public void setPitch(float pitch) {

            enforceCaller();

            mPitch =
                    clamp(pitch);

            applyToNative();

            saveConfig();
        }

        @Override
        public void setFormant(float formant) {

            enforceCaller();

            mFormant =
                    clamp(formant);

            applyToNative();

            saveConfig();
        }

        @Override
        public void setVariation(float variation) {

            enforceCaller();

            mVariation =
                    Math.max(
                            0.0f,
                            Math.min(
                                    0.010f,
                                    variation
                            )
                    );

            applyToNative();

            saveConfig();
        }
    };

    private void enforceCaller() {

        int uid = Binder.getCallingUid();

        if (uid == Process.SYSTEM_UID) {
            return;
        }

        try {

            String[] pkgs =
                    getContext()
                            .getPackageManager()
                            .getPackagesForUid(uid);

            if (pkgs != null) {

                for (String p : pkgs) {

                    if (ALLOWED_PACKAGE.equals(p)) {

                        if (getContext()
                                .getPackageManager()
                                .checkSignatures(
                                        p,
                                        "android"
                                )
                                == android.content.pm.PackageManager
                                        .SIGNATURE_MATCH) {

                            return;
                        }
                    }
                }
            }

        } catch (Throwable ignored) {
        }

        throw new SecurityException(
                "SecureLineVoice: unauthorized caller uid="
                        + uid
        );
    }


    private float clamp(float v) {

        if (v < MIN_VALUE) {
            return MIN_VALUE;
        }

        if (v > MAX_VALUE) {
            return MAX_VALUE;
        }

        return v;
    }

    private void applyMode(String mode) {

        switch (mode) {

            case "DEEP_VOICE":

                /*
                 * Natural deep profile:
                 * clearly lower and fuller than the source voice,
                 * but still designed for everyday intelligibility.
                 */
                mPitch = 1.22f;
                mFormant = 0.91f;
                mVariation = 0.00035f;
                mWarmth = 0.20f;
                mCompression = 0.38f;
                mDrive = 0.08f;
                mOutputGain = 0.94f;
                mSpectralTilt = -0.18f;
                mPresence = 0.03f;

                break;

            case "DOCUMENTARY_VOICE":

                /*
                 * Signature dark documentary / anonymous interview voice.
                 * Strong low vocal character with controlled dynamics.
                 */
                mPitch = 1.39f;
                mFormant = 0.80f;
                mVariation = 0.00065f;
                mWarmth = 0.34f;
                mCompression = 0.58f;
                mDrive = 0.13f;
                mOutputGain = 0.91f;
                mSpectralTilt = -0.44f;
                mPresence = -0.08f;

                break;

            case "PRIVATE_VOICE":

                /*
                 * Strong identity-changing profile.
                 * More pitch/formant separation plus subtle motion and
                 * a deliberately different spectral balance.
                 */
                mPitch = 1.31f;
                mFormant = 0.75f;
                mVariation = 0.00175f;
                mWarmth = 0.20f;
                mCompression = 0.50f;
                mDrive = 0.11f;
                mOutputGain = 0.91f;
                mSpectralTilt = -0.28f;
                mPresence = 0.08f;

                break;

            case "CLEAR_VOICE":

                /*
                 * First light profile:
                 * noticeably brighter and higher than the source voice,
                 * while avoiding a helium / cartoon sound.
                 */
                mPitch = 0.91f;
                mFormant = 1.09f;
                mVariation = 0.00055f;
                mWarmth = 0.05f;
                mCompression = 0.36f;
                mDrive = 0.04f;
                mOutputGain = 0.94f;
                mSpectralTilt = 0.24f;
                mPresence = 0.18f;

                break;

            case "BRIGHT_VOICE":

                /*
                 * Second light profile:
                 * stronger identity separation, higher vocal character,
                 * reduced chest tone and a more open spectral balance.
                 */
                mPitch = 0.81f;
                mFormant = 1.18f;
                mVariation = 0.00120f;
                mWarmth = 0.02f;
                mCompression = 0.42f;
                mDrive = 0.035f;
                mOutputGain = 0.92f;
                mSpectralTilt = 0.46f;
                mPresence = 0.24f;

                break;

	    case "GHOST_VOICE":

	        /*
	         * Rugged tactical identity-protection profile.
	         * Deep, raspy and controlled with strong intelligibility
	         * and clear separation from Documentary Voice.
	         */
	        mPitch = 1.47f;
	        mFormant = 0.82f;
	        mVariation = 0.00095f;

	        mWarmth = 0.20f;
	        mCompression = 0.56f;
	        mDrive = 0.20f;

	        mOutputGain = 0.92f;
	        mSpectralTilt = -0.18f;
	        mPresence = 0.26f;

	        break;

            default:

                mMode = "DEEP_VOICE";

                mPitch = 1.22f;
                mFormant = 0.91f;
                mVariation = 0.00035f;
                mWarmth = 0.20f;
                mCompression = 0.38f;
                mDrive = 0.08f;
                mOutputGain = 0.94f;
                mSpectralTilt = -0.18f;
                mPresence = 0.03f;

                break;
        }
    }

    private void applyToNative() {

        try {

            SystemProperties.set(
                    "persist.audio.secureline.enabled",
                    mEnabled ? "1" : "0"
            );

            // V5: the old forced AudioPolicy/PatchPanel cellular bridge is permanently disabled.
            // SIM Voice uses Android's native PSTN call-redirection injection path instead.
            SystemProperties.set(
                    "persist.audio.secureline.cellular_enabled",
                    "0"
            );
SystemProperties.set(
                    "persist.audio.secureline.pitch",
                    Float.toString(mPitch)
            );

            SystemProperties.set(
                    "persist.audio.secureline.formant",
                    Float.toString(mFormant)
            );

            SystemProperties.set(
                    "persist.audio.secureline.variation",
                    Float.toString(mVariation)
            );

            SystemProperties.set(
                    "persist.audio.secureline.warmth",
                    Float.toString(mWarmth)
            );

            SystemProperties.set(
                    "persist.audio.secureline.compression",
                    Float.toString(mCompression)
            );

            SystemProperties.set(
                    "persist.audio.secureline.drive",
                    Float.toString(mDrive)
            );

            SystemProperties.set(
                    "persist.audio.secureline.output_gain",
                    Float.toString(mOutputGain)
            );

            SystemProperties.set(
                    "persist.audio.secureline.spectral_tilt",
                    Float.toString(mSpectralTilt)
            );

            SystemProperties.set(
                    "persist.audio.secureline.presence",
                    Float.toString(mPresence)
            );

            SystemProperties.set(
                    "persist.audio.secureline.refresh",
                    Long.toString(
                            System.nanoTime()
                    )
            );

        } catch (Throwable t) {

            Slog.e(
                    TAG,
                    "applyToNative failed",
                    t
            );
        }
    }

    private void loadConfig() {

        try {

            if (!mConfigFile.exists()) {
                return;
            }

            byte[] data =
                    Files.readAllBytes(
                            mConfigFile.toPath()
                    );

            JSONObject obj =
                    new JSONObject(
                            new String(
                                    data,
                                    StandardCharsets.UTF_8
                            )
                    );

            mEnabled =
                    obj.optBoolean(
                            "enabled",
                            false
                    );
mMode =
                    obj.optString(
                            "mode",
                            "DEEP_VOICE"
                    );

            mPitch =
                    clamp(
                            (float) obj.optDouble(
                                    "pitch",
                                    1.0
                            )
                    );

            mFormant =
                    clamp(
                            (float) obj.optDouble(
                                    "formant",
                                    1.0
                            )
                    );

            mVariation =
                    Math.max(
                            0.0f,
                            Math.min(
                                    0.010f,
                                    (float) obj.optDouble(
                                            "variation",
                                            DEFAULT_VARIATION
                                    )
                            )
                    );

            mWarmth =
                    (float) obj.optDouble(
                            "warmth",
                            0.16
                    );

            mCompression =
                    (float) obj.optDouble(
                            "compression",
                            0.35
                    );

            mDrive =
                    (float) obj.optDouble(
                            "drive",
                            0.08
                    );

            mOutputGain =
                    (float) obj.optDouble(
                            "outputGain",
                            0.95
                    );

            mSpectralTilt =
                    (float) obj.optDouble(
                            "spectralTilt",
                            -0.10
                    );

            mPresence =
                    (float) obj.optDouble(
                            "presence",
                            0.05
                    );

        } catch (Throwable e) {

            Slog.e(
                    TAG,
                    "config load failed",
                    e
            );
        }
    }

    private void saveConfig() {

        try {

            JSONObject obj =
                    new JSONObject();

            obj.put(
                    "enabled",
                    mEnabled
            );
obj.put(
                    "mode",
                    mMode
            );

            obj.put(
                    "pitch",
                    mPitch
            );

            obj.put(
                    "formant",
                    mFormant
            );

            obj.put(
                    "variation",
                    mVariation
            );

            obj.put(
                    "warmth",
                    mWarmth
            );

            obj.put(
                    "compression",
                    mCompression
            );

            obj.put(
                    "drive",
                    mDrive
            );

            obj.put(
                    "outputGain",
                    mOutputGain
            );

            obj.put(
                    "spectralTilt",
                    mSpectralTilt
            );

            obj.put(
                    "presence",
                    mPresence
            );

            byte[] data =
                    obj.toString()
                            .getBytes(
                                    StandardCharsets.UTF_8
                            );

            File tmp =
                    new File(
                            mConfigFile.getAbsolutePath()
                                    + ".tmp"
                    );

            try (FileOutputStream fos =
                         new FileOutputStream(tmp)) {

                fos.write(data);
                fos.flush();
                fos.getFD().sync();
            }

            if (!tmp.renameTo(mConfigFile)) {

                throw new IllegalStateException(
                        "atomic rename failed"
                );
            }

        } catch (Throwable e) {

            Slog.e(
                    TAG,
                    "config save failed",
                    e
            );
        }
    }
}
