#!/usr/bin/env python3
"""
Password-gated browser panel (multi-account, accounts stay logged in).

- ONE password locks/unlocks the PANEL. Wrong password -> decoy ("Hello Rajendra").
- Lock only hides the panel behind the password again. It does NOT log out your
  accounts. Your WhatsApps / Telegram / Email / Hosting stay signed in, so you do
  not re-login every time.
- Each app opens in its OWN session, so 5 WhatsApp numbers (or many Telegrams)
  can all be logged in at once, each separate.
- URL bar to browse any site. Voice button -> say an app name to open it.

SECURITY:
- The panel password is HASHED in config.json (real password never stored,
  a hash can't be reversed, so the file reveals nothing).
- Getting into your accounts requires the panel password first.
  (Because accounts stay logged in, keep this password private and don't leave
   the panel unlocked on a shared computer.)

Install once:
    pip install PyQt5 PyQtWebEngine
    pip install SpeechRecognition pyaudio      # optional, only for the Voice button

Run:
    python panel.py
"""

import json
import hashlib
import os
import sys

from PyQt5.QtCore import Qt, QThread, pyqtSignal, QUrl
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit,
    QPushButton, QLabel, QStackedWidget, QMessageBox, QScrollArea, QShortcut
)
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage

BASE = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(BASE, "config.json")
SESS_DIR = os.path.join(BASE, ".sessions")   # saved logins per app live here

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")


def load_config():
    with open(CONFIG_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def sha(text):
    return hashlib.sha256(text.encode()).hexdigest()


class VoiceThread(QThread):
    heard = pyqtSignal(str)
    failed = pyqtSignal(str)

    def run(self):
        try:
            import speech_recognition as sr
        except Exception:
            self.failed.emit("Voice needs:  pip install SpeechRecognition pyaudio")
            return
        try:
            r = sr.Recognizer()
            with sr.Microphone() as source:
                r.adjust_for_ambient_noise(source, duration=0.4)
                audio = r.listen(source, timeout=5, phrase_time_limit=4)
            self.heard.emit(r.recognize_google(audio).lower())
        except Exception as e:
            self.failed.emit("Didn't catch that (" + type(e).__name__ + ")")


class Panel(QWidget):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.apps = config.get("apps", [])
        self.views = {}       # index -> QWebEngineView
        self.profiles = {}    # index -> QWebEngineProfile

        self.setWindowTitle(config.get("window_title", "Login"))
        self.resize(1050, 720)

        self.stack = QStackedWidget()
        root = QVBoxLayout(self)
        root.setContentsMargins(0, 0, 0, 0)
        root.addWidget(self.stack)

        self.stack.addWidget(self._build_login())   # index 0
        self.browser_page = None

        # F11 toggles full screen, Esc leaves it
        QShortcut(QKeySequence("F11"), self, activated=self.toggle_fullscreen)
        QShortcut(QKeySequence("Esc"), self, activated=self.showMaximized)

    def toggle_fullscreen(self):
        if self.isFullScreen():
            self.showMaximized()
        else:
            self.showFullScreen()

    # ---------- login ----------
    def _build_login(self):
        page = QWidget()
        lay = QVBoxLayout(page)
        lay.setAlignment(Qt.AlignCenter)
        lay.setContentsMargins(60, 60, 60, 60)
        lay.setSpacing(16)

        title = QLabel("Login")
        title.setStyleSheet("font-size:22px; font-weight:600;")
        title.setAlignment(Qt.AlignCenter)

        self.pw = QLineEdit()
        self.pw.setEchoMode(QLineEdit.Password)
        self.pw.setPlaceholderText("Password")
        self.pw.setFixedWidth(280)
        self.pw.setStyleSheet("padding:8px; font-size:14px;")
        self.pw.returnPressed.connect(self.check_password)

        btn = QPushButton("Enter")
        btn.setFixedWidth(280)
        btn.setStyleSheet("padding:8px; font-size:14px;")
        btn.clicked.connect(self.check_password)

        self.info = QLabel("")
        self.info.setAlignment(Qt.AlignCenter)
        self.info.setStyleSheet("font-family:Consolas,monospace; font-size:13px; color:#333;")

        for w in (title, self.pw, btn, self.info):
            lay.addWidget(w, alignment=Qt.AlignCenter)
        return page

    def check_password(self):
        pw = self.pw.text()
        self.pw.clear()
        if sha(pw) in self.config.get("password_hashes", []):
            self.info.setText("")
            self.unlock()
        else:
            decoy = self.config.get("decoy", {})
            lines = [decoy.get("greeting", "Hello")] + decoy.get("details", [])
            self.info.setText("\n".join(lines))

    # ---------- browser (built once, kept alive so accounts stay logged in) ----------
    def unlock(self):
        if self.browser_page is None:
            self.browser_page = self._build_browser()
            self.stack.addWidget(self.browser_page)     # index 1
            if self.apps:
                self.open_app(0)
        self.stack.setCurrentIndex(1)

    def _build_browser(self):
        page = QWidget()
        lay = QVBoxLayout(page)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(0)

        # app buttons (scrollable row)
        btn_row = QWidget()
        brow = QHBoxLayout(btn_row)
        brow.setContentsMargins(6, 4, 6, 4)
        brow.setSpacing(6)
        for i, app in enumerate(self.apps):
            b = QPushButton(app.get("name", "App %d" % (i + 1)))
            b.setStyleSheet("padding:6px 12px;")
            b.clicked.connect(lambda _, idx=i: self.open_app(idx))
            brow.addWidget(b)
        brow.addStretch()

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFixedHeight(46)
        scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll.setWidget(btn_row)
        lay.addWidget(scroll)

        # url + voice + lock
        bar = QHBoxLayout()
        bar.setContentsMargins(8, 4, 8, 6)
        bar.setSpacing(8)

        self.url = QLineEdit()
        self.url.setPlaceholderText("Type a web address and press Enter")
        self.url.setStyleSheet("padding:6px; font-size:13px;")
        self.url.returnPressed.connect(self.go_url)
        bar.addWidget(self.url, stretch=1)

        self.voice_btn = QPushButton("🎤 Voice")
        self.voice_btn.setStyleSheet("padding:6px 14px;")
        self.voice_btn.clicked.connect(self.start_voice)
        bar.addWidget(self.voice_btn)

        lock = QPushButton("Lock Panel")
        lock.setStyleSheet("padding:6px 14px;")
        lock.clicked.connect(self.lock)
        bar.addWidget(lock)

        lay.addLayout(bar)

        self.content = QStackedWidget()
        lay.addWidget(self.content, stretch=1)
        return page

    def open_app(self, index):
        if index not in self.views:
            # persistent profile per app -> stays logged in, and separate from others
            store = os.path.join(SESS_DIR, "acct_%d" % index)
            os.makedirs(store, exist_ok=True)
            profile = QWebEngineProfile("acct_%d" % index, self)
            profile.setPersistentStoragePath(store)
            profile.setCachePath(store)
            profile.setHttpUserAgent(UA)
            profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies)

            view = _WebView(profile)
            view.urlChanged.connect(lambda qurl, v=view: self._sync_url(v))
            view.load_url(self.apps[index]["url"])
            self.profiles[index] = profile
            self.views[index] = view
            self.content.addWidget(view)
        view = self.views[index]
        self.content.setCurrentWidget(view)
        self._sync_url(view)

    def _sync_url(self, view):
        if self.content.currentWidget() is view:
            self.url.setText(view.url().toString())

    def go_url(self):
        text = self.url.text().strip()
        if not text:
            return
        if "://" not in text:
            text = "https://" + text
        w = self.content.currentWidget()
        if isinstance(w, _WebView):
            w.load_url(text)

    def lock(self):
        # hide panel behind the password again; accounts stay logged in
        self.info.setText("")
        self.stack.setCurrentIndex(0)
        self.pw.setFocus()

    # ---------- voice ----------
    def start_voice(self):
        self.voice_btn.setText("Listening…")
        self.voice_btn.setEnabled(False)
        self.vt = VoiceThread()
        self.vt.heard.connect(self.on_voice)
        self.vt.failed.connect(self.on_voice_fail)
        self.vt.start()

    def _reset_voice_btn(self):
        self.voice_btn.setText("🎤 Voice")
        self.voice_btn.setEnabled(True)

    def on_voice(self, text):
        self._reset_voice_btn()
        for i, app in enumerate(self.apps):
            if app.get("name", "").lower() in text:
                self.open_app(i)
                return
        for i, app in enumerate(self.apps):
            if any(w in text for w in app.get("name", "").lower().split()):
                self.open_app(i)
                return
        QMessageBox.information(self, "Voice", 'Heard: "%s" — no matching app.' % text)

    def on_voice_fail(self, msg):
        self._reset_voice_btn()
        QMessageBox.information(self, "Voice", msg)


class _WebView(QWebEngineView):
    def __init__(self, profile):
        super().__init__()
        self.setPage(QWebEnginePage(profile, self))

    def load_url(self, url):
        self.load(QUrl(url))


def main():
    cfg = load_config()
    app = QApplication(sys.argv)
    w = Panel(cfg)
    w.showMaximized()          # open big; press F11 for true full screen, Esc to exit
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
