# onionfy/client.py
import subprocess
import socket
import time
import os
import requests
import atexit
import configparser
from pathlib import Path


class OnionClient:
    def __init__(
        self,
        config_path=None,
        tor_path=None,
        socks_host=None,
        socks_port=None,
        auto_start=None,
        timeout=None,
        verbose=False,
        exit_nodes=None,
        exclude_exit_nodes=None,
        strict_nodes=None,
    ):
        self.verbose = verbose
        self._log("[Onionfy] Initializing OnionClient...")
        
        config = self._load_config(config_path)
        
        self.tor_path = tor_path or config.get('tor', 'tor_path')
        self._log(f"[Onionfy] Config tor_path: {self.tor_path}")
        
        if not os.path.exists(self.tor_path):
            self._log(f"[Onionfy] Tor not found at config path, auto-detecting...")
            detected_path = self._find_tor_executable()
            if detected_path:
                self._log(f"[Onionfy] Tor detected at: {detected_path}")
                self.tor_path = detected_path
            else:
                self._log(f"[Onionfy] Tor not found anywhere")
        else:
            self._log(f"[Onionfy] Tor found at: {self.tor_path}")
        
        self.socks_host = socks_host or config.get('tor', 'socks_host')
        self.socks_port = socks_port or config.getint('tor', 'socks_port')
        self.timeout = timeout or config.getint('tor', 'timeout')
        auto_start = auto_start if auto_start is not None else config.getboolean('tor', 'auto_start')
        
        self.control_host = config.get('tor', 'control_host')
        self.control_port = config.getint('tor', 'control_port')
        self.control_password = config.get('tor', 'control_password')
        
        self._log(f"[Onionfy] SOCKS proxy: {self.socks_host}:{self.socks_port}")
        
        self.max_retries = config.getint('network', 'max_retries')
        self.request_timeout = config.getint('network', 'request_timeout')
        self.verify_ssl = config.getboolean('network', 'verify_ssl')
        
        self.exit_nodes = exit_nodes if exit_nodes is not None else config.get('network', 'exit_nodes')
        self.exclude_exit_nodes = exclude_exit_nodes if exclude_exit_nodes is not None else config.get('network', 'exclude_exit_nodes')
        self.strict_nodes = strict_nodes if strict_nodes is not None else config.getboolean('network', 'strict_nodes')
        
        self.user_agent = config.get('privacy', 'user_agent')
        self.disable_cookies = config.getboolean('privacy', 'disable_cookies')
        self.disable_cache = config.getboolean('privacy', 'disable_cache')
        
        self.proc = None

        self.session = requests.Session()
        self.session.proxies.update(self.proxies)
        self.session.headers.update({'User-Agent': self.user_agent})
        
        if self.disable_cookies:
            self.session.cookies.clear()
        if self.disable_cache:
            self.session.headers.update({'Cache-Control': 'no-cache'})

        if auto_start:
            self._log("[Onionfy] Auto-start enabled, starting Tor...")
            self.start()
        else:
            self._log("[Onionfy] Auto-start disabled")

        atexit.register(self.stop)
        self._log("[Onionfy] Initialization complete")
    
    def _log(self, message):
        """Print log message if verbose mode is enabled."""
        if self.verbose:
            print(message)
    
    def _find_tor_executable(self):
        """Auto-detect Tor executable location."""
        self._log("[Onionfy] Trying 'where tor.exe' command...")
        try:
            result = subprocess.run(
                ['where', 'tor.exe'],
                capture_output=True,
                text=True,
                timeout=2
            )
            if result.returncode == 0 and result.stdout.strip():
                path = result.stdout.strip().split('\n')[0]
                self._log(f"[Onionfy] Found via 'where': {path}")
                return path
        except Exception as e:
            self._log(f"[Onionfy] 'where' command failed: {e}")
        
        self._log("[Onionfy] Checking common installation paths...")
        common_paths = [
            r"C:\Program Files\Tor Browser\Browser\TorBrowser\Tor\tor.exe",
            r"C:\Program Files (x86)\Tor Browser\Browser\TorBrowser\Tor\tor.exe",
            Path.home() / "Desktop" / "Tor Browser" / "Browser" / "TorBrowser" / "Tor" / "tor.exe",
            Path.home() / "Downloads" / "Tor Browser" / "Browser" / "TorBrowser" / "Tor" / "tor.exe",
            r"C:\Tor\tor.exe",
        ]
        
        for path in common_paths:
            self._log(f"[Onionfy] Checking: {path}")
            if os.path.exists(path):
                self._log(f"[Onionfy] Found at: {path}")
                return str(path)
        
        self._log("[Onionfy] Tor not found in any common location")
        return None
    
    def _load_config(self, config_path=None):
        """Load configuration from file."""
        config = configparser.ConfigParser()
        
        default_config = Path(__file__).parent / 'onionfy.conf'
        config.read(default_config)
        
        if config_path is None:
            search_paths = [
                Path.cwd() / 'onionfy.conf',
                Path.home() / '.onionfy.conf',
            ]
            for path in search_paths:
                if path.exists():
                    config_path = path
                    break
        
        if config_path and Path(config_path).exists():
            config.read(config_path)
        
        return config

    @property
    def proxy_url(self):
        """Returns SOCKS5 proxy URL for use with any library."""
        return f"socks5h://{self.socks_host}:{self.socks_port}"

    @property
    def proxies(self):
        """Returns proxy dict for requests library."""
        return {
            "http": self.proxy_url,
            "https": self.proxy_url,
        }

    def _is_running(self):
        try:
            s = socket.create_connection((self.socks_host, self.socks_port), 1)
            s.close()
            return True
        except OSError:
            return False

    def start(self):
        """Start Tor process if not already running."""
        self._log("[Onionfy] Checking if Tor is already running...")
        if self._is_running():
            self._log("[Onionfy] Tor is already running, skipping start")
            return

        self._log(f"[Onionfy] Tor not running, starting from: {self.tor_path}")
        if not os.path.exists(self.tor_path):
            raise FileNotFoundError(
                f"Tor executable not found at: {self.tor_path}\n"
                f"Please install Tor Browser from: https://www.torproject.org/download/\n"
                f"Or set custom path: OnionClient(tor_path='C:\\\\path\\\\to\\\\tor.exe')"
            )
        
        torrc_path = None
        if self.exit_nodes or self.exclude_exit_nodes:
            torrc_path = self._generate_torrc()

        try:
            self._log("[Onionfy] Launching Tor process...")
            cmd = [self.tor_path]
            if torrc_path:
                cmd.extend(['-f', torrc_path])
            
            self.proc = subprocess.Popen(
                cmd,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )
            self._log(f"[Onionfy] Tor process started (PID: {self.proc.pid})")
        except Exception as e:
            raise RuntimeError(f"Failed to start Tor: {e}")

        self._log(f"[Onionfy] Waiting for Tor to be ready (timeout: {self.timeout}s)...")
        start = time.time()
        while time.time() - start < self.timeout:
            if self._is_running():
                elapsed = time.time() - start
                self._log(f"[Onionfy] Tor is ready! (took {elapsed:.1f}s)")
                return
            time.sleep(1)

        self._log("[Onionfy] Tor failed to start within timeout")
        self.stop()
        raise RuntimeError("Tor failed to start within timeout")
    
    def _generate_torrc(self):
        """Generate custom torrc file with exit node preferences."""
        import tempfile
        import shutil
        
        data_dir = tempfile.mkdtemp(prefix='tor_data_')
        
        geoip_dir = Path(__file__).parent / 'geoip'
        geoip_file = geoip_dir / "geoip"
        geoip6_file = geoip_dir / "geoip6"
        template_torrc = geoip_dir / "torrc"
        
        if not geoip_file.exists():
            raise FileNotFoundError(
                f"GeoIP file not found at: {geoip_file}\n"
                f"Exit nodes require GeoIP files in the library."
            )
        
        self._log(f"[Onionfy] Using GeoIP: {geoip_file}")
        self._log(f"[Onionfy] Using GeoIPv6: {geoip6_file}")
        
        torrc_content = f"""DataDirectory {data_dir}
GeoIPFile {geoip_file}
GeoIPv6File {geoip6_file}
SocksPort {self.socks_port}
ControlPort {self.control_port}
Log notice file {data_dir}\\tor.log
"""
        
        if self.exit_nodes:
            countries = [c.strip() for c in self.exit_nodes.split(',')]
            formatted_nodes = ','.join([f'{{{c}}}' for c in countries])
            torrc_content += f"ExitNodes {formatted_nodes}\n"
            self._log(f"[Onionfy] Exit nodes: {formatted_nodes}")
        
        if self.exclude_exit_nodes:
            countries = [c.strip() for c in self.exclude_exit_nodes.split(',')]
            formatted_nodes = ','.join([f'{{{c}}}' for c in countries])
            torrc_content += f"ExcludeExitNodes {formatted_nodes}\n"
            self._log(f"[Onionfy] Exclude exit nodes: {formatted_nodes}")
        
        if self.strict_nodes:
            torrc_content += "StrictNodes 1\n"
            self._log("[Onionfy] Strict nodes: enabled")
        
        fd, path = tempfile.mkstemp(suffix='.torrc', text=True)
        with os.fdopen(fd, 'w') as f:
            f.write(torrc_content)
        
        self._log(f"[Onionfy] Generated custom torrc at: {path}")
        self._log(f"[Onionfy] Tor data directory: {data_dir}")
        
        if self.verbose:
            self._log("[Onionfy] Torrc content:")
            for line in torrc_content.split('\n'):
                if line.strip():
                    self._log(f"  {line}")
        
        return path

    def stop(self):
        """Stop Tor process."""
        if self.proc and self.proc.poll() is None:
            self.proc.terminate()
            self.proc.wait(timeout=5)

    def check_connection(self):
        """Verify Tor connection and return current IP."""
        try:
            response = self.session.get("https://check.torproject.org/api/ip", timeout=10)
            data = response.json()
            return {"ip": data.get("IP"), "is_tor": data.get("IsTor", False)}
        except Exception as e:
            raise RuntimeError(f"Failed to verify Tor connection: {e}")

    def set_system_proxy(self):
        """Set environment variables so other libraries use Tor automatically."""
        os.environ['http_proxy'] = self.proxy_url
        os.environ['https_proxy'] = self.proxy_url
        os.environ['HTTP_PROXY'] = self.proxy_url
        os.environ['HTTPS_PROXY'] = self.proxy_url
    
    def set_exit_nodes(self, exit_nodes=None, exclude_exit_nodes=None, strict_nodes=None):
        """Change exit node configuration and restart Tor."""
        self._log("[Onionfy] Changing exit node configuration...")
        
        if exit_nodes is not None:
            self.exit_nodes = exit_nodes
        if exclude_exit_nodes is not None:
            self.exclude_exit_nodes = exclude_exit_nodes
        if strict_nodes is not None:
            self.strict_nodes = strict_nodes
        
        self._log(f"[Onionfy] New exit nodes: {self.exit_nodes}")
        self._log(f"[Onionfy] New exclude: {self.exclude_exit_nodes}")
        
        self._log("[Onionfy] Stopping current Tor instance...")
        self.stop()
        time.sleep(2)
        
        self._log("[Onionfy] Restarting Tor with new exit nodes...")
        self.start()
        
        self._log("[Onionfy] Exit nodes updated successfully")
    
    def renew_identity(self):
        """Request new Tor identity (change IP address)."""
        try:
            self._log("[Onionfy] Requesting new Tor identity...")
            import socket as sock
            with sock.socket(sock.AF_INET, sock.SOCK_STREAM) as s:
                s.connect((self.control_host, self.control_port))
                
                if self.control_password:
                    s.send(f'AUTHENTICATE "{self.control_password}"\r\n'.encode())
                else:
                    s.send(b'AUTHENTICATE\r\n')
                
                response = s.recv(1024).decode()
                if '250 OK' not in response:
                    raise RuntimeError(f"Authentication failed: {response}")
                
                s.send(b'SIGNAL NEWNYM\r\n')
                response = s.recv(1024).decode()
                
                if '250 OK' in response:
                    self._log("[Onionfy] New identity obtained, waiting 5s for circuit...")
                    time.sleep(1)
                    return True
                else:
                    raise RuntimeError(f"NEWNYM failed: {response}")
        except Exception as e:
            self._log(f"[Onionfy] Failed to renew identity: {e}")
            raise RuntimeError(f"Failed to renew Tor identity: {e}")

    def get(self, url, **kwargs):
        """HTTP GET request through Tor."""
        kwargs.setdefault('timeout', self.request_timeout)
        kwargs.setdefault('verify', self.verify_ssl)
        
        for attempt in range(self.max_retries):
            try:
                return self.session.get(url, **kwargs)
            except Exception as e:
                if attempt == self.max_retries - 1:
                    raise
                self._log(f"[Onionfy] Request failed (attempt {attempt + 1}/{self.max_retries}): {e}")
                time.sleep(1)

    def post(self, url, **kwargs):
        """HTTP POST request through Tor."""
        kwargs.setdefault('timeout', self.request_timeout)
        kwargs.setdefault('verify', self.verify_ssl)
        
        for attempt in range(self.max_retries):
            try:
                return self.session.post(url, **kwargs)
            except Exception as e:
                if attempt == self.max_retries - 1:
                    raise
                self._log(f"[Onionfy] Request failed (attempt {attempt + 1}/{self.max_retries}): {e}")
                time.sleep(1)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop()
