import CleanupModels
import Foundation
import LoggingModels
import SharedConstants

// MARK: - Sample Code Cleaner

extension Sample.Cleanup {
    /// Actor for cleaning sample code archives by removing unnecessary files
    /// such as .git folders, .DS_Store, build artifacts, etc. Previously
    /// `Cleanup.SampleCodeCleaner`; nested under `Sample.Cleanup` per the
    /// cross-cutting Sample namespace policy.
    public actor Cleaner {
        private let sampleCodeDirectory: URL
        private let dryRun: Bool
        private let keepOriginals: Bool
        /// #656 — when true, dry-run runs `zipinfo` on every archive to
        /// count items that would be removed (accurate but slow,
        /// ~minutes on a 600-zip corpus). When false (the default for
        /// dry-run after #656), each archive is reported with
        /// `itemsRemoved: 0` and the subprocess fan-out is skipped
        /// entirely — dry-run finishes in seconds. Real cleanup
        /// (dryRun = false) ignores this flag; it always runs the full
        /// extract → scan → recompress pipeline.
        private let verify: Bool
        /// GoF Strategy seam for log emission (1994 p. 315).
        private let logger: any LoggingModels.Logging.Recording

        /// Patterns of files/folders to remove from archives
        /// Standard gitignore patterns - safe to remove (build artifacts, not source code)
        private static let cleanupPatterns: [String] = [
            ".git",
            ".DS_Store",
            "DerivedData",
            "build",
            ".build",
            "xcuserdata",
            "*.xcuserstate",
        ]

        public init(
            sampleCodeDirectory: URL,
            dryRun: Bool = false,
            keepOriginals: Bool = false,
            verify: Bool = false,
            logger: any LoggingModels.Logging.Recording
        ) {
            self.sampleCodeDirectory = sampleCodeDirectory
            self.dryRun = dryRun
            self.keepOriginals = keepOriginals
            self.verify = verify
            self.logger = logger
        }

        // MARK: - Public Methods

        /// Clean all ZIP archives in the sample code directory. Pass
        /// an `any Sample.Cleanup.CleanerProgressObserving` to receive
        /// per-archive progress updates; `nil` opts out.
        public func cleanup(
            progress: (any Sample.Cleanup.CleanerProgressObserving)? = nil
        ) async throws -> Shared.Models.CleanupStatistics {
            let startTime = Date()

            // Find all ZIP files
            let zipFiles = try findZipFiles()

            guard !zipFiles.isEmpty else {
                logger.info("No ZIP files found in \(sampleCodeDirectory.path)", category: .samples)
                return Shared.Models.CleanupStatistics(
                    totalArchives: 0,
                    cleanedArchives: 0,
                    skippedArchives: 0,
                    errors: 0,
                    originalTotalSize: 0,
                    cleanedTotalSize: 0,
                    duration: Date().timeIntervalSince(startTime)
                )
            }

            logger.info("Found \(zipFiles.count) ZIP archives to process", category: .samples)

            var cleanedArchives = 0
            var skippedArchives = 0
            var errors = 0
            var originalTotalSize: Int64 = 0
            var cleanedTotalSize: Int64 = 0
            var totalItemsRemoved = 0

            for (index, zipFile) in zipFiles.enumerated() {
                let result = await cleanArchive(at: zipFile)

                originalTotalSize += result.originalSize
                cleanedTotalSize += result.cleanedSize
                totalItemsRemoved += result.itemsRemoved

                if result.success {
                    if result.itemsRemoved > 0 {
                        cleanedArchives += 1
                    } else {
                        skippedArchives += 1
                    }
                } else {
                    errors += 1
                    if let error = result.errorMessage {
                        logger.error("Failed to clean \(zipFile.lastPathComponent): \(error)", category: .samples)
                    }
                }

                progress?.observe(progress: Shared.Models.CleanupProgress(
                    current: index + 1,
                    total: zipFiles.count,
                    currentFile: zipFile.lastPathComponent,
                    originalSize: originalTotalSize,
                    cleanedSize: cleanedTotalSize
                ))
            }

            return Shared.Models.CleanupStatistics(
                totalArchives: zipFiles.count,
                cleanedArchives: cleanedArchives,
                skippedArchives: skippedArchives,
                errors: errors,
                originalTotalSize: originalTotalSize,
                cleanedTotalSize: cleanedTotalSize,
                totalItemsRemoved: totalItemsRemoved,
                duration: Date().timeIntervalSince(startTime)
            )
        }

        // MARK: - Private Methods

        /// Find all ZIP files in the sample code directory
        private func findZipFiles() throws -> [URL] {
            let contents = try FileManager.default.contentsOfDirectory(
                at: sampleCodeDirectory,
                includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey],
                options: [.skipsHiddenFiles]
            )

            return contents.filter { $0.pathExtension.lowercased() == "zip" }
                .sorted { $0.lastPathComponent < $1.lastPathComponent }
        }

        /// Clean a single archive
        private func cleanArchive(at zipURL: URL) async -> Shared.Models.CleanupResult {
            let filename = zipURL.lastPathComponent

            // Get original size
            let originalSize: Int64
            do {
                let attributes = try FileManager.default.attributesOfItem(atPath: zipURL.path)
                originalSize = (attributes[.size] as? Int64) ?? 0
            } catch {
                return Shared.Models.CleanupResult(
                    filename: filename,
                    originalSize: 0,
                    cleanedSize: 0,
                    itemsRemoved: 0,
                    success: false,
                    errorMessage: "Failed to get file size: \(error.localizedDescription)"
                )
            }

            // Dry run - just report what would be cleaned.
            //
            // #656 — pre-fix every dry-run archive ran `/usr/bin/zipinfo`
            // in a subprocess, fanning out 627 fork+exec calls on the
            // default corpus and turning a "preview" into ~3 minutes of
            // work. The throttled output (#651) hid the firehose but
            // didn't speed up the underlying scan. Default behaviour is
            // now stat-only: report each archive at its on-disk size +
            // `itemsRemoved: 0` without ever opening the zip. The
            // archive count + size totals stay accurate (that's what
            // most operators want from `--dry-run` — "do I have a
            // corpus to clean?"). When the user opts into precise
            // counting via `--verify`, we fall through to the old path.
            if dryRun {
                let itemsToRemove = verify ? await countItemsToRemove(in: zipURL) : 0
                return Shared.Models.CleanupResult(
                    filename: filename,
                    originalSize: originalSize,
                    cleanedSize: originalSize,
                    itemsRemoved: itemsToRemove,
                    success: true
                )
            }

            // Create temporary directory for extraction
            let tempDir = FileManager.default.temporaryDirectory
                .appendingPathComponent(UUID().uuidString)

            defer {
                try? FileManager.default.removeItem(at: tempDir)
            }

            do {
                // Create temp directory
                try FileManager.default.createDirectory(
                    at: tempDir,
                    withIntermediateDirectories: true
                )

                // Extract ZIP
                let extractResult = try await extractZip(zipURL, to: tempDir)
                guard extractResult else {
                    return Shared.Models.CleanupResult(
                        filename: filename,
                        originalSize: originalSize,
                        cleanedSize: originalSize,
                        itemsRemoved: 0,
                        success: false,
                        errorMessage: "Failed to extract archive"
                    )
                }

                // Remove unwanted files/folders
                let itemsRemoved = try removeUnwantedItems(in: tempDir)

                // Skip recompression if nothing was removed
                if itemsRemoved == 0 {
                    return Shared.Models.CleanupResult(
                        filename: filename,
                        originalSize: originalSize,
                        cleanedSize: originalSize,
                        itemsRemoved: 0,
                        success: true
                    )
                }

                // Recompress
                let cleanedZipURL: URL
                if keepOriginals {
                    cleanedZipURL = zipURL.deletingPathExtension()
                        .appendingPathExtension("cleaned.zip")
                } else {
                    cleanedZipURL = zipURL
                }

                // Remove original if replacing
                if !keepOriginals {
                    try FileManager.default.removeItem(at: zipURL)
                }

                let compressResult = try await compressDirectory(tempDir, to: cleanedZipURL)
                guard compressResult else {
                    return Shared.Models.CleanupResult(
                        filename: filename,
                        originalSize: originalSize,
                        cleanedSize: originalSize,
                        itemsRemoved: itemsRemoved,
                        success: false,
                        errorMessage: "Failed to recompress archive"
                    )
                }

                // Get new size
                let newAttributes = try FileManager.default.attributesOfItem(atPath: cleanedZipURL.path)
                let cleanedSize = (newAttributes[.size] as? Int64) ?? 0

                return Shared.Models.CleanupResult(
                    filename: filename,
                    originalSize: originalSize,
                    cleanedSize: cleanedSize,
                    itemsRemoved: itemsRemoved,
                    success: true
                )

            } catch {
                return Shared.Models.CleanupResult(
                    filename: filename,
                    originalSize: originalSize,
                    cleanedSize: originalSize,
                    itemsRemoved: 0,
                    success: false,
                    errorMessage: error.localizedDescription
                )
            }
        }

        /// Extract ZIP archive using ditto (preserves permissions and attributes)
        private func extractZip(_ zipURL: URL, to destination: URL) async throws -> Bool {
            let process = Process()
            process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
            process.arguments = ["-xk", zipURL.path, destination.path]

            try process.run()
            process.waitUntilExit()

            return process.terminationStatus == 0
        }

        /// Compress directory to ZIP using ditto
        private func compressDirectory(_ directory: URL, to zipURL: URL) async throws -> Bool {
            // Sample code ZIPs extract flat (no wrapper directory)
            // All files (.git, source dirs, xcodeproj, etc.) are at temp directory root
            // We need to compress ALL contents together, not pick a single directory

            let process = Process()
            process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")

            // Compress the directory itself (which contains all the extracted files)
            // This recreates the flat structure in the ZIP
            process.arguments = ["-ck", directory.path, zipURL.path]

            try process.run()
            process.waitUntilExit()

            return process.terminationStatus == 0
        }

        /// Remove unwanted items from extracted directory
        private func removeUnwantedItems(in directory: URL) throws -> Int {
            var itemsRemoved = 0

            guard let enumerator = FileManager.default.enumerator(
                at: directory,
                includingPropertiesForKeys: [.isDirectoryKey],
                options: []
            ) else {
                return 0
            }

            var itemsToRemove: [URL] = []

            for case let fileURL as URL in enumerator {
                let name = fileURL.lastPathComponent

                if shouldRemove(name: name, url: fileURL) {
                    itemsToRemove.append(fileURL)

                    // Skip enumeration inside directories we're removing
                    let resourceValues = try? fileURL.resourceValues(forKeys: [.isDirectoryKey])
                    if resourceValues?.isDirectory == true {
                        enumerator.skipDescendants()
                    }
                }
            }

            // Sort by path length (longest first) to remove nested items before parents
            itemsToRemove.sort { $0.path.count > $1.path.count }

            for itemURL in itemsToRemove {
                do {
                    try FileManager.default.removeItem(at: itemURL)
                    itemsRemoved += 1
                    logger.debug("Removed: \(itemURL.path)", category: .samples)
                } catch {
                    logger.warning("Failed to remove \(itemURL.path): \(error)", category: .samples)
                }
            }

            return itemsRemoved
        }

        /// Check if a file/folder should be removed
        private func shouldRemove(name: String, url: URL) -> Bool {
            for pattern in Self.cleanupPatterns {
                if pattern.contains("*") {
                    // Wildcard pattern
                    let regex = pattern
                        .replacingOccurrences(of: ".", with: "\\.")
                        .replacingOccurrences(of: "*", with: ".*")
                    if name.range(of: "^\(regex)$", options: .regularExpression) != nil {
                        return true
                    }
                } else {
                    // Exact match
                    if name == pattern {
                        return true
                    }
                }
            }
            return false
        }

        /// Count items that would be removed (for dry run)
        private func countItemsToRemove(in zipURL: URL) async -> Int {
            // For dry run, we use zipinfo to inspect without extracting
            let process = Process()
            process.executableURL = URL(fileURLWithPath: "/usr/bin/zipinfo")
            process.arguments = ["-1", zipURL.path]

            let pipe = Pipe()
            process.standardOutput = pipe

            do {
                try process.run()
                process.waitUntilExit()

                let data = pipe.fileHandleForReading.readDataToEndOfFile()
                guard let output = String(data: data, encoding: .utf8) else {
                    return 0
                }

                let files = output.components(separatedBy: "\n")
                var count = 0

                for file in files where !file.isEmpty {
                    if shouldRemovePath(file) {
                        count += 1
                    }
                }

                return count
            } catch {
                return 0
            }
        }

        /// Check if a path should be removed (checks all path components)
        private func shouldRemovePath(_ path: String) -> Bool {
            let components = path.components(separatedBy: "/")
            for component in components {
                if shouldRemove(name: component, url: URL(fileURLWithPath: path)) {
                    return true
                }
            }
            return false
        }
    }
}
