import Foundation

public actor SyncEngine {
    private let client: APIClient
    private let config: SyncConfig
    private let localState: LocalSyncState
    private var snapshotId: String?
    
    public init(client: APIClient, config: SyncConfig) {
        self.client = client
        self.config = config
        self.localState = LocalSyncState(deviceId: config.deviceId)
    }
    
    public func performSync() async throws {
        guard let syncFolder = config.syncFolder else {
            return
        }
        
        let folderURL = URL(fileURLWithPath: syncFolder)
        try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true)
        
        // 1. Initialize sync
        if snapshotId == nil {
            let initResponse = try await client.syncInit(deviceId: config.deviceId, rootPath: syncFolder)
            self.snapshotId = initResponse.snapshotId
            try await applyInitialServerSnapshot(initResponse.serverSnapshot, baseURL: folderURL)
            try await localState.updateSnapshot(initResponse.serverSnapshot)
        }
        
        guard let currentSnapshotId = snapshotId else { return }
        
        // 2. Compute local delta
        let localFiles = try scanLocalFolder(folderURL)
        let localDelta = try await localState.computeDelta(localFiles: localFiles)
        let outboundDelta = try addContent(to: localDelta, baseURL: folderURL)

        // 3. Send local changes and check for server changes
        let deltaResponse = try await client.syncDelta(snapshotId: currentSnapshotId, clientDelta: outboundDelta)
        
        // 4. Apply server changes
        try await applyServerDelta(deltaResponse.serverDelta, baseURL: folderURL)

        // The server returns its current bytes in serverDelta for conflicts.
        // Until the native app has a merge UI, use the server copy locally.
        self.snapshotId = deltaResponse.newSnapshotId ?? currentSnapshotId

        // 5. Update local state after applying inbound changes
        try await localState.updateSnapshot(scanLocalFolder(folderURL))
    }
    
    private func scanLocalFolder(_ folderURL: URL) throws -> [FileEntry] {
        var files: [FileEntry] = []
        let fileManager = FileManager.default
        
        guard let enumerator = fileManager.enumerator(
            at: folderURL,
            includingPropertiesForKeys: [.contentModificationDateKey, .fileSizeKey],
            options: [.skipsHiddenFiles]
        ) else {
            return files
        }
        
        for case let fileURL as URL in enumerator {
            let relativePath = fileURL.path.replacingOccurrences(of: folderURL.path + "/", with: "")
            
            // Only sync markdown files for now
            guard fileURL.pathExtension == "md" else { continue }
            
            let attributes = try fileManager.attributesOfItem(atPath: fileURL.path)
            let modificationDate = attributes[.modificationDate] as? Date ?? Date()
            let fileSize = attributes[.size] as? Int64 ?? 0
            
            // Compute hash
            let content = try Data(contentsOf: fileURL)
            let hash = SHA256.hash(data: content).hexString
            
            files.append(FileEntry(
                path: relativePath,
                hash: hash,
                size: fileSize,
                modified: modificationDate
            ))
        }
        
        return files
    }
    
    private func applyServerDelta(_ changes: [Change], baseURL: URL) async throws {
        for change in changes {
            let fileURL = try localFileURL(for: change.path, baseURL: baseURL)
            
            switch change.type {
            case "create", "update":
                guard let hash = change.hash else {
                    throw SyncEngineError.missingHash(change.path)
                }
                try await writeServerContent(hash: hash, to: fileURL)
            case "delete":
                try? FileManager.default.removeItem(at: fileURL)
            case "rename":
                if let oldPath = change.oldPath {
                    let oldURL = try localFileURL(for: oldPath, baseURL: baseURL)
                    try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
                    try? FileManager.default.moveItem(at: oldURL, to: fileURL)
                }
            default:
                break
            }
        }
    }

    private func applyInitialServerSnapshot(_ files: [FileEntry], baseURL: URL) async throws {
        for file in files {
            let fileURL = try localFileURL(for: file.path, baseURL: baseURL)
            if !FileManager.default.fileExists(atPath: fileURL.path) {
                try await writeServerContent(hash: file.hash, to: fileURL)
            }
        }
    }

    private func addContent(to changes: [Change], baseURL: URL) throws -> [Change] {
        try changes.map { change in
            guard change.type == "create" || change.type == "update" else {
                return change
            }

            let fileURL = try localFileURL(for: change.path, baseURL: baseURL)
            let data = try Data(contentsOf: fileURL)
            guard let content = String(data: data, encoding: .utf8) else {
                throw SyncEngineError.invalidUTF8(change.path)
            }

            return Change(
                type: change.type,
                path: change.path,
                oldPath: change.oldPath,
                hash: change.hash,
                content: content,
                size: change.size,
                modified: change.modified
            )
        }
    }

    private func writeServerContent(hash: String, to fileURL: URL) async throws {
        let content = try await client.fetchContent(hash: hash)
        guard SHA256.hash(data: content).hexString == hash.lowercased() else {
            throw SyncEngineError.hashMismatch(fileURL.lastPathComponent)
        }
        try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
        try content.write(to: fileURL, options: .atomic)
    }

    private func localFileURL(for path: String, baseURL: URL) throws -> URL {
        let root = baseURL.standardizedFileURL
        let fileURL = root.appendingPathComponent(path).standardizedFileURL
        guard fileURL.path.hasPrefix(root.path + "/"),
              fileURL.pathExtension.lowercased() == "md" else {
            throw SyncEngineError.invalidPath(path)
        }
        return fileURL
    }
}

public enum SyncEngineError: LocalizedError {
    case invalidPath(String)
    case invalidUTF8(String)
    case missingHash(String)
    case hashMismatch(String)

    public var errorDescription: String? {
        switch self {
        case .invalidPath(let path):
            return "Invalid sync path: \(path)"
        case .invalidUTF8(let path):
            return "Sync only supports UTF-8 markdown files: \(path)"
        case .missingHash(let path):
            return "Server sync response is missing a content hash: \(path)"
        case .hashMismatch(let path):
            return "Downloaded sync content failed hash verification: \(path)"
        }
    }
}

// MARK: - SHA256 Helper

import CryptoKit

extension SHA256.Digest {
    var hexString: String {
        map { String(format: "%02x", $0) }.joined()
    }
}

// MARK: - Local Sync State

public actor LocalSyncState {
    private let deviceId: String
    private var lastSnapshot: [String: FileEntry] = [:] // path -> entry
    
    public init(deviceId: String) {
        self.deviceId = deviceId
    }
    
    public func updateSnapshot(_ files: [FileEntry]) async throws {
        lastSnapshot = Dictionary(uniqueKeysWithValues: files.map { ($0.path, $0) })
        try persist()
    }
    
    public func computeDelta(localFiles: [FileEntry]) async throws -> [Change] {
        var changes: [Change] = []
        let localMap = Dictionary(uniqueKeysWithValues: localFiles.map { ($0.path, $0) })
        
        // Check for new or modified files
        for file in localFiles {
            if let last = lastSnapshot[file.path] {
                if last.hash != file.hash {
                    changes.append(Change(
                        type: "update",
                        path: file.path,
                        oldPath: nil,
                        hash: file.hash,
                        size: file.size,
                        modified: file.modified
                    ))
                }
            } else {
                changes.append(Change(
                    type: "create",
                    path: file.path,
                    oldPath: nil,
                    hash: file.hash,
                    size: file.size,
                    modified: file.modified
                ))
            }
        }
        
        // Check for deleted files
        for (path, _) in lastSnapshot where localMap[path] == nil {
            changes.append(Change(
                type: "delete",
                path: path,
                oldPath: nil,
                hash: nil,
                size: nil,
                modified: nil
            ))
        }
        
        return changes
    }
    
    private func persist() throws {
        // Persist to local JSON file
        let entries = Array(lastSnapshot.values)
        let data = try JSONEncoder().encode(entries)
        let url = stateFileURL()
        try data.write(to: url)
    }
    
    private func stateFileURL() -> URL {
        let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
        let dir = appSupport.appendingPathComponent("CairnquireSync", isDirectory: true)
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        return dir.appendingPathComponent("sync_state_\(deviceId).json")
    }
}
