<?php

declare(strict_types=1);

namespace SiteFort\Modules\Scanner;

use SiteFort\Modules\Scanner\Service\IssueType;
use SiteFort\Modules\Scanner\Repository\IssueRepository;
use SiteFort\Modules\Scanner\Service\ScanLogger;

if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * Detects sensitive files that are publicly accessible.
 */
class SensitiveDataScanner  {

    /**
     * Get the scanner title.
     * 
     * @return string
     */
    public function get_title() {
        return 'Sensitive Data Exposure';
    }

    /**
     * Run the scan.
     * 
     * @return array List of ScanResult arrays
     */
    public function run() {
        if ( class_exists( ScanLogger::class ) ) {
            ScanLogger::log( "Checking for exposed sensitive files..." );
        }

        $issues = [];
        $checks = $this->get_checks();

        foreach ( $checks as $check ) {
            if ( ! file_exists( $check['path'] ) ) {
                continue;
            }

            if ( class_exists( ScanLogger::class ) ) {
                ScanLogger::log( "Found: {$check['file']} - verifying accessibility..." );
            }

            if ( $this->is_publicly_accessible( $check['url'] ) ) {
                $issues[] = $this->create_result( $check );
                if ( class_exists( ScanLogger::class ) ) {
                    ScanLogger::log( "⚠ Exposed file confirmed: {$check['file']}" );
                }
            } else {
                if ( class_exists( ScanLogger::class ) ) {
                    ScanLogger::log( "File protected (not accessible). Skipping." );
                }
            }
        }

        return $issues;
    }

    /**
     * Build a targeted list of sensitive files to check for public access.
     *
     * @return array
     */
    private function get_checks() {
        $checks = [];
        $root = trailingslashit( ABSPATH );

        $content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ( $root . 'wp-content' );
        $content_dir = trailingslashit( $content_dir );

        $uploads_base = '';
        if ( function_exists( 'wp_get_upload_dir' ) ) {
            $uploads = wp_get_upload_dir();
            if ( ! empty( $uploads['basedir'] ) ) {
                $uploads_base = trailingslashit( $uploads['basedir'] );
            }
        }

        // Critical config files - we check ONLY backup/exposed variants, not the legitimate files
        // wp-config.php is excluded - the legitimate file shouldn't be accessible anyway
        // Backup variants are caught dynamically via glob patterns (*.bak, *.old, *~ etc.)
        $root_files = [
            // Environment files (these should never exist or be accessible)
            '.env',
            '.env.local',
            '.env.production',
            '.env.development',
            '.env.dev',
            '.git/config',
            '.git/HEAD',
            '.git/index',
            '.svn/entries',
            '.htaccess',
            '.htpasswd',
            'composer.json',
            'composer.lock',
            'package.json',
            'yarn.lock',
            'phpinfo.php',
            'info.php',
            'adminer.php',
            'searchreplace.php',
            'error_log',
            'backup.sql',
            'database.sql',
            'dump.sql',
            'db.sql',
            'backup.zip',
            'site.zip',
            'site.tar.gz',
            // OS and editor metadata files
            '.DS_Store',
            'Thumbs.db',
            // Environment backup variants
            '.env.bak',
            // Config backup variants
            'wp-config.php.bak',
            'wp-config.old',
        ];

        foreach ( $root_files as $file ) {
            $this->add_check_path( $checks, $root . $file );
        }

        // IDE and editor directory config files
        $ide_files = [
            ".idea/workspace.xml",
            ".idea/modules.xml",
            ".vscode/settings.json",
            ".vscode/launch.json",
        ];

        foreach ( $ide_files as $file ) {
            $this->add_check_path( $checks, $root . $file );
        }

        $content_files = [
            'debug.log',
            'error_log',
        ];

        foreach ( $content_files as $file ) {
            $this->add_check_path( $checks, $content_dir . $file );
        }

        $patterns = [
            '*.sql',
            '*.sql.gz',
            '*.sql.zip',
            '*.zip',
            '*.tar',
            '*.tar.gz',
            '*.tgz',
            '*.gz',
            '*.rar',
            '*.7z',
            '*.bak',
            '*.backup',
            '*.old',
            '*~',
            '*.log',
            // Config file variants - dynamically catch wp-config.*, wp-config-*, etc.
            'wp-config.*',      // Catches wp-config.php.bak, wp-config.old, etc.
            'wp-config-*',      // Catches wp-config-backup.php, wp-config-sample.php, etc.
            '.htaccess.*',      // Catches .htaccess.bak, .htaccess.old, etc.
            // Vim swap files
            '*.swp',
        ];

        $scan_dirs = [
            $root,
            $content_dir,
        ];

        if ( ! empty( $uploads_base ) ) {
            $scan_dirs[] = $uploads_base;
        }

        $scan_dirs[] = $content_dir . 'backups';
        $scan_dirs[] = $content_dir . 'backup';
        $scan_dirs[] = $content_dir . 'uploads/backups';

        $remaining = 80;
        foreach ( $scan_dirs as $dir ) {
            if ( $remaining <= 0 ) {
                break;
            }
            $remaining = $this->add_glob_checks( $checks, $dir, $patterns, $remaining );
        }

        return array_values( $checks );
    }

    private function add_check_path( array &$checks, $path ) {
        if ( empty( $path ) ) {
            return;
        }

        $normalized = wp_normalize_path( $path );
        if ( isset( $checks[ $normalized ] ) ) {
            return;
        }

        $url = $this->path_to_url( $normalized );
        if ( empty( $url ) ) {
            return;
        }

        $checks[ $normalized ] = [
            'file' => $this->path_to_relative( $normalized ),
            'path' => $normalized,
            'url'  => $url,
        ];
    }

    private function add_glob_checks( array &$checks, $dir, array $patterns, $limit = 40 ) {
        if ( empty( $dir ) || ! is_dir( $dir ) ) {
            return $limit;
        }

        $base = trailingslashit( $dir );

        foreach ( $patterns as $pattern ) {
            if ( $limit <= 0 ) {
                break;
            }

            $matches = glob( $base . $pattern, GLOB_NOSORT );
            if ( empty( $matches ) ) {
                continue;
            }

            foreach ( $matches as $match ) {
                if ( $limit <= 0 ) {
                    break;
                }
                // Filter out symlinks to prevent following them
                if ( is_link( $match ) ) {
                    continue;
                }
                if ( is_file( $match ) ) {
                    // Exclude legitimate files (not backups)
                    $basename = basename( $match );
                    if ( in_array( $basename, [
                        'wp-config.php',         // Legitimate config
                        'wp-config-sample.php',  // Legitimate sample file
                        '.htaccess',             // Legitimate htaccess
                    ], true ) ) {
                        continue;
                    }
                    $this->add_check_path( $checks, $match );
                    $limit--;
                }
            }
        }

        return $limit;
    }

    private function path_to_url( $path ) {
        $root = wp_normalize_path( trailingslashit( ABSPATH ) );
        $content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ( $root . 'wp-content' );
        $content_dir = wp_normalize_path( trailingslashit( $content_dir ) );

        $normalized = wp_normalize_path( $path );

        if ( strpos( $normalized, $content_dir ) === 0 ) {
            $rel = ltrim( substr( $normalized, strlen( $content_dir ) ), '/' );
            return content_url( '/' . $rel );
        }

        if ( strpos( $normalized, $root ) === 0 ) {
            $rel = ltrim( substr( $normalized, strlen( $root ) ), '/' );
            return site_url( '/' . $rel );
        }

        return '';
    }

    private function path_to_relative( $path ) {
        $root = wp_normalize_path( trailingslashit( ABSPATH ) );
        $normalized = wp_normalize_path( $path );

        if ( strpos( $normalized, $root ) === 0 ) {
            return ltrim( substr( $normalized, strlen( $root ) ), '/' );
        }

        $real = realpath( $path );
        if ( $real !== false ) {
            $real = wp_normalize_path( $real );
            if ( strpos( $real, $root ) === 0 ) {
                return ltrim( substr( $real, strlen( $root ) ), '/' );
            }
        }

        if ( strpos( $normalized, '/' ) !== 0 ) {
            return ltrim( $normalized, '/' );
        }

        return $normalized;
    }

    private function is_publicly_accessible( $url ) {
        if ( empty( $url ) ) {
            return false;
        }

        $args = [
            'timeout'     => 6,
            'redirection' => 2,
            'headers'     => [
                'Cache-Control' => 'no-cache',
                'Pragma'        => 'no-cache',
            ],
        ];

        $response = wp_remote_head( $url, $args );
        if ( is_wp_error( $response ) ) {
            return false;
        }

        $code = (int) wp_remote_retrieve_response_code( $response );

        if ( $code === 405 || $code === 0 ) {
            $response = wp_remote_get( $url, [
                'timeout'     => 6,
                'redirection' => 2,
                'headers'     => [
                    'Range'         => 'bytes=0-0',
                    'Cache-Control' => 'no-cache',
                    'Pragma'        => 'no-cache',
                ],
            ] );

            if ( is_wp_error( $response ) ) {
                return false;
            }

            $code = (int) wp_remote_retrieve_response_code( $response );
        }

        return ( $code >= 200 && $code < 300 ) || $code === 206;
    }

    /**
     * Create ScanResult array with context-aware description.
     * 
     * @param array $check
     * @return array
     */
    private function create_result( $check ) {
        $data = $this->get_contextual_data( $check['file'] );
        $remediation = $this->get_remediation_plan( $check['file'] );

        return [
            'id'          => uniqid( 'sensitive_' ),
            'hash'        => IssueRepository::generateHash( 'sensitive_data', $check['file'] ),
            'type'        => IssueType::SENSITIVE_DATA,
            'status'      => 'warning',
            'severity'    => 'high',
            'file'        => $check['path'],
            'message'     => $data['title'],
            'description' => $data['description'],
            'details'     => $data['description'],
            'created_at'  => time(),
            'slug'        => 'sensitive_data',
            'data'        => [
                'url' => $check['url'],
                'description' => $data['description'],
                'threat_context' => $data['description'],
                'exposure_label' => $data['context_label'],
                'exposure_value' => $data['context_value'],
                'preferred_action' => $remediation['primary'],
                'available_actions' => $remediation['actions'],
            ],
            'meta'        => [
                'label'  => 'Sensitive File Exposure: ' . $check['file'],
                'layout' => 'generic_card',
                'actions' => $remediation['actions']
            ]
        ];
    }

    /**
     * Determine remediation strategy by file risk profile.
     * Backend is source-of-truth for UI actions.
     *
     * @param string $filename Relative file path.
     * @return array{primary:string,actions:array}
     */
    private function get_remediation_plan( $filename ) {
        $lower = strtolower( (string) $filename );
        $ext = strtolower( (string) pathinfo( $lower, PATHINFO_EXTENSION ) );

        // Request-driven policy:
        // - config/log/sql/git class exposures: protect access first
        // - archives/backups: delete first
        $protect_first = (
            strpos( $lower, '.env' ) !== false ||
            strpos( $lower, '.git/' ) !== false ||
            strpos( $lower, '.svn/' ) !== false ||
            strpos( $lower, 'wp-config' ) !== false ||
            strpos( $lower, '.htaccess' ) !== false ||
            strpos( $lower, '.htpasswd' ) !== false ||
            strpos( $lower, 'composer.' ) !== false ||
            strpos( $lower, 'package.json' ) !== false ||
            strpos( $lower, 'yarn.lock' ) !== false ||
            $ext === 'log' ||
            strpos( $lower, 'log' ) !== false ||
            $ext === 'sql' ||
            strpos( $lower, '.sql' ) !== false
        );

        $delete_first = in_array( $ext, [ 'zip', 'tar', 'gz', 'tgz', 'rar', '7z', 'bak', 'backup', 'old' ], true );

        if ( $delete_first && ! $protect_first ) {
            return [
                'primary' => 'delete',
                'actions' => [ 'delete', 'ignore' ],
            ];
        }

        return [
            'primary' => 'protect',
            'actions' => [ 'protect', 'delete', 'view', 'ignore' ],
        ];
    }

    /**
     * Generate professional title and description based on file type.
     */
    private function get_contextual_data( $filename ) {
        $ext = strtolower( (string) pathinfo( $filename, PATHINFO_EXTENSION ) );
        $lower_name = strtolower( $filename );

        if (
            strpos( $lower_name, 'wp-config' ) !== false ||
            strpos( $lower_name, '.env' ) !== false ||
            strpos( $lower_name, '.htaccess' ) !== false ||
            strpos( $lower_name, '.htpasswd' ) !== false
        ) {
            return [
                'title' => "Exposed Critical Configuration: {$filename}",
                'description' => "This file contains security-sensitive configuration values. Public access can reveal credentials, keys, and server directives that attackers can use to compromise the site.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Critical Configuration File',
            ];
        }

        if ( strpos( $lower_name, '.git/' ) !== false || strpos( $lower_name, '.svn/' ) !== false ) {
            return [
                'title' => "Exposed Repository Metadata: {$filename}",
                'description' => "Repository metadata is publicly reachable. Attackers can use this to reconstruct source history and discover secrets, internal paths, or deployment details.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Source Control Metadata',
            ];
        }

        if ( $ext === 'sql' || strpos( $lower_name, 'dump' ) !== false || strpos( $lower_name, 'database' ) !== false ) {
            return [
                'title' => "Exposed Database Dump: {$filename}",
                'description' => "A database export is publicly accessible. This may expose user records, password hashes, customer data, and internal configuration data.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Database Export File',
            ];
        }

        if ( in_array( $ext, [ 'zip', 'tar', 'gz', 'tgz', 'rar', '7z', 'bak', 'backup', 'old' ], true ) ) {
            return [
                'title' => "Exposed Site Archive: {$filename}",
                'description' => "A backup or archive file is publicly downloadable. This can disclose source code, plugin data, and sensitive configuration that should never be public.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Backup / Archive File',
            ];
        }

        if ( $ext === 'log' || strpos( $lower_name, 'log' ) !== false ) {
            return [
                'title' => "Exposed Log File: {$filename}",
                'description' => "A log file is publicly accessible. Logs often expose internal paths, stack traces, plugin names, and operational metadata that can accelerate targeted attacks.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Operational Log File',
            ];
        }

        if (
            $lower_name === 'package.json' ||
            $lower_name === 'composer.json' ||
            $lower_name === 'composer.lock' ||
            $lower_name === 'yarn.lock'
        ) {
            return [
                'title' => "Exposed Project Metadata: {$filename}",
                'description' => "Dependency metadata is exposed publicly. Attackers can fingerprint package versions and target known vulnerabilities in your software stack.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Dependency Metadata File',
            ];
        }

        if ( strpos( $lower_name, 'info.php' ) !== false ) {
            return [
                'title' => "Exposed Server Information: {$filename}",
                'description' => "A diagnostic endpoint is exposed publicly. It reveals runtime and environment details that can be used to craft server-specific exploit attempts.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Server Diagnostics Endpoint',
            ];
        }

        if ( strpos( $lower_name, 'adminer' ) !== false || strpos( $lower_name, 'searchreplace' ) !== false ) {
            return [
                'title' => "Exposed Admin Utility: {$filename}",
                'description' => "A database or maintenance utility is publicly exposed. If accessed by unauthorized users, it can enable direct data modification or system abuse.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Administrative Utility',
            ];
        }

        if ( strpos( $lower_name, '.idea/' ) !== false || strpos( $lower_name, '.vscode/' ) !== false ) {
            return [
                'title' => "Exposed Development Metadata: {$filename}",
                'description' => "Development workspace metadata is publicly exposed. While not always critical, it can reveal project structure and internal tooling information.",
                'context_label' => 'Exposure Type',
                'context_value' => 'Development Metadata File',
            ];
        }

        // Default
        return [
            'title' => "Publicly Accessible Sensitive File: {$filename}",
            'description' => "This file is publicly accessible and may expose internal or sensitive operational data. Public access should be restricted unless explicitly required.",
            'context_label' => 'Exposure Type',
            'context_value' => 'Sensitive Public File',
        ];
    }
}
