#!/usr/bin/env python3
"""
Xprotec V1

Protect a website zip package for demo distribution.
"""

from __future__ import annotations

import argparse
import secrets
import shutil
import sys
import tempfile
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile


HTACCESS_CONTENT = """DirectoryIndex index.php
Options -Indexes

<IfModule mod_rewrite.c>
RewriteEngine On

# Force entry through PHP controller only.
RewriteRule ^index\\.html$ - [F,L,NC]
RewriteRule ^security-config\\.php$ - [F,L,NC]
RewriteRule ^protect-build\\.php$ - [F,L,NC]
RewriteRule ^_xprotec_[A-Za-z0-9]+/ - [F,L,NC]

# Per-directory rules (works in root and subfolders).
RewriteRule ^(img|audio|assets)/ - [L,NC]
RewriteRule ^(favicon\\.ico|index\\.php)$ - [L,NC]
RewriteRule . index.php [L]
</IfModule>

<FilesMatch "\\.(html|md|map)$">
Require all denied
</FilesMatch>
"""


SECURITY_CONFIG_TEMPLATE = """<?php
declare(strict_types=1);

return [
    // IMPORTANT: change this secret before deployment.
    'secret' => '{secret}',

    // Signed URL gate: https://example.com/?exp=1719999999&sig=...
    'require_signed_url' => false,

    // Empty = no host filtering. Add exact hosts in production.
    'allowed_hosts' => [
        // 'demo.yourdomain.com',
    ],

    // Optional local bypass for development/testing.
    'allow_localhost_without_signature' => true,

    // In seconds: max tolerated server/client drift around exp.
    'clock_skew_seconds' => 120,

    // Hidden protected source storage (random folder name per build).
    'protected_storage_dir' => __DIR__ . '/{storage_dir}',
    'source_html_path' => __DIR__ . '/{storage_dir}/index.html',
    'source_php_path' => __DIR__ . '/{storage_dir}/app.php',
];
"""


INDEX_GATE_TEMPLATE = """<?php
declare(strict_types=1);

$config = require __DIR__ . '/security-config.php';

function deny(int $status, string $message): void
{
    http_response_code($status);
    header('Content-Type: text/plain; charset=UTF-8');
    echo $message;
    exit;
}

function isLocalRequest(): bool
{
    $host = strtolower((string)($_SERVER['HTTP_HOST'] ?? ''));
    return str_contains($host, 'localhost') || str_contains($host, '127.0.0.1');
}

function validateHost(array $allowedHosts): bool
{
    if (!$allowedHosts) return true;
    $currentHost = strtolower((string)($_SERVER['HTTP_HOST'] ?? ''));
    return in_array($currentHost, array_map('strtolower', $allowedHosts), true);
}

function expectedSignature(string $secret, int $exp): string
{
    return hash_hmac('sha256', (string)$exp, $secret);
}

function withDevtoolsDeterrent(string $html): string
{
    $deterrent = <<<'JS'
<script>
(() => {
  document.addEventListener('contextmenu', (e) => e.preventDefault());
  document.addEventListener('keydown', (e) => {
    const k = (e.key || '').toLowerCase();
    if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && ['i','j','c'].includes(k)) || (e.ctrlKey && ['u','s'].includes(k))) {
      e.preventDefault();
      e.stopPropagation();
      return false;
    }
  }, true);
})();
</script>
JS;
    if (stripos($html, '</body>') !== false) {
        return preg_replace('/<\\/body>/i', $deterrent . "\\n</body>", $html, 1) ?? ($html . $deterrent);
    }
    return $html . $deterrent;
}

header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer');
header("Content-Security-Policy: default-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com; img-src 'self' data:; media-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:;");
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');

if (!validateHost((array)($config['allowed_hosts'] ?? []))) {
    deny(403, 'Host not allowed.');
}

$isLocal = isLocalRequest();
$allowLocalBypass = (bool)($config['allow_localhost_without_signature'] ?? false);
$requireSignedUrl = (bool)($config['require_signed_url'] ?? true);

if ($requireSignedUrl && !($isLocal && $allowLocalBypass)) {
    $exp = (int)($_GET['exp'] ?? 0);
    $sig = (string)($_GET['sig'] ?? '');
    $now = time();
    $skew = (int)($config['clock_skew_seconds'] ?? 120);
    $expected = expectedSignature((string)$config['secret'], $exp);

    if ($exp <= 0 || $sig === '') deny(403, 'Missing signed URL params.');
    if ($now > ($exp + $skew)) deny(403, 'Demo link expired.');
    if (!hash_equals($expected, $sig)) deny(403, 'Invalid signature.');
}

if ('{mode}' === 'php') {
    $sourcePath = (string)($config['source_php_path'] ?? (__DIR__ . '/app.php'));
    if (!is_file($sourcePath)) deny(500, 'Protected app not found.');
    require $sourcePath;
    exit;
}

$sourcePath = (string)($config['source_html_path'] ?? (__DIR__ . '/index.html'));
if (!is_file($sourcePath)) deny(500, 'Protected source not found.');
$html = file_get_contents($sourcePath);
if ($html === false) deny(500, 'Unable to read protected source.');
header('Content-Type: text/html; charset=UTF-8');
echo withDevtoolsDeterrent($html);
"""


PROTECT_BUILD_TEMPLATE = """<?php
declare(strict_types=1);

$config = require __DIR__ . '/security-config.php';
$baseUrl = $argv[1] ?? '';
$exp = isset($argv[2]) ? (int)$argv[2] : (time() + 3600);

if ($baseUrl === '') {
    fwrite(STDERR, "Usage: php protect-build.php <baseUrl> [expTimestamp]\\n");
    exit(1);
}

$parts = parse_url($baseUrl);
if (!$parts || !isset($parts['scheme'], $parts['host'])) {
    fwrite(STDERR, "Invalid URL.\\n");
    exit(1);
}

$sig = hash_hmac('sha256', (string)$exp, (string)$config['secret']);
$sep = str_contains($baseUrl, '?') ? '&' : '?';
$signed = $baseUrl . $sep . 'exp=' . rawurlencode((string)$exp) . '&sig=' . rawurlencode($sig);

echo $signed . PHP_EOL;
"""


DEPLOY_README = """Xprotec package
================

1) Upload all files to your server.
2) Edit security-config.php:
   - set a strong secret
   - set allowed_hosts
3) Generate signed URL:
   php protect-build.php "https://demo.yourdomain.com/"
4) Share only signed links.
"""


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Protect a website zip package (Xprotec)")
    parser.add_argument("input_zip", type=Path, help="Path to source website .zip")
    parser.add_argument("--output", type=Path, default=None, help="Output protected .zip path")
    return parser.parse_args()


def ensure_supported_entry(root: Path) -> str:
    has_php = (root / "index.php").exists()
    has_html = (root / "index.html").exists()
    if has_php:
        return "php"
    if has_html:
        return "html"
    raise RuntimeError("Input zip must contain index.php or index.html at root.")


def unpack_zip(input_zip: Path, target: Path) -> None:
    with ZipFile(input_zip, "r") as zf:
        zf.extractall(target)


def pack_zip(source_dir: Path, out_zip: Path) -> None:
    with ZipFile(out_zip, "w", compression=ZIP_DEFLATED) as zf:
        for path in source_dir.rglob("*"):
            if path.is_file():
                zf.write(path, path.relative_to(source_dir))


def protect_site(work_dir: Path, mode: str) -> None:
    storage_dir_name = f"_xprotec_{secrets.token_hex(10)}"
    storage_dir = work_dir / storage_dir_name
    storage_dir.mkdir(parents=True, exist_ok=True)

    if mode == "php":
        src = work_dir / "index.php"
        dst = storage_dir / "app.php"
        if dst.exists():
            raise RuntimeError("Protected app.php already exists in storage folder.")
        src.rename(dst)
    else:
        src = work_dir / "index.html"
        dst = storage_dir / "index.html"
        if dst.exists():
            raise RuntimeError("Protected index.html already exists in storage folder.")
        src.rename(dst)

    secret = secrets.token_hex(32)
    (work_dir / ".htaccess").write_text(HTACCESS_CONTENT, encoding="utf-8")
    (work_dir / "security-config.php").write_text(
        SECURITY_CONFIG_TEMPLATE.format(secret=secret, storage_dir=storage_dir_name), encoding="utf-8"
    )
    gate_php = INDEX_GATE_TEMPLATE.replace("{mode}", mode)
    (work_dir / "index.php").write_text(gate_php, encoding="utf-8")
    (work_dir / "protect-build.php").write_text(PROTECT_BUILD_TEMPLATE, encoding="utf-8")
    (work_dir / "README-XPROTEC.txt").write_text(DEPLOY_README, encoding="utf-8")


def default_output(input_zip: Path) -> Path:
    suffix = "-xprotec.zip"
    if input_zip.name.lower().endswith(".zip"):
        return input_zip.with_name(input_zip.stem + suffix)
    return input_zip.with_name(input_zip.name + suffix)


def main() -> int:
    args = parse_args()
    input_zip = args.input_zip
    output_zip = args.output or default_output(input_zip)

    if not input_zip.exists():
        print(f"Error: input not found: {input_zip}", file=sys.stderr)
        return 1
    if input_zip.suffix.lower() != ".zip":
        print("Error: input must be a .zip file", file=sys.stderr)
        return 1

    with tempfile.TemporaryDirectory(prefix="xprotec-") as tmp:
        tmp_dir = Path(tmp)
        unpack_dir = tmp_dir / "site"
        unpack_dir.mkdir(parents=True, exist_ok=True)
        unpack_zip(input_zip, unpack_dir)

        mode = ensure_supported_entry(unpack_dir)
        protect_site(unpack_dir, mode)

        output_zip.parent.mkdir(parents=True, exist_ok=True)
        if output_zip.exists():
            output_zip.unlink()
        pack_zip(unpack_dir, output_zip)

    print(f"Protected package generated: {output_zip}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
