import Foundation
import SampleIndexModels
import SharedConstants

// MARK: - Sample Search

// `Sample.Search.Query` and `Sample.Search.Result` lifted to the
// `SampleIndexModels` target so callers (`SearchToolProvider` and
// future MCP / CLI surfaces) can construct queries without importing
// the full `Services` target. The actor below stays here because it
// holds behaviour over `any Sample.Index.Reader`.

// MARK: - Sample Search Service

/// Service for searching Apple sample code projects and files.
/// Wraps Sample.Index.Database with a clean interface.
extension Sample.Search {
    public actor Service {
        private let database: any Sample.Index.Reader

        /// Initialize with an existing database. Accepts any
        /// `Sample.Index.Reader` so this layer doesn't depend on the
        /// concrete `Sample.Index.Database` actor — the composition
        /// root supplies it.
        public init(database: any Sample.Index.Reader) {
            self.database = database
        }

        // MARK: - Search Methods

        /// Search with a specialized query
        ///
        /// #732 expansion: the 5-field platform filter on
        /// `Sample.Search.Query` (`minIOS` / `minMacOS` / `minTvOS` /
        /// `minWatchOS` / `minVisionOS`) is threaded into both
        /// `searchProjects` and `searchFiles`. Multiple `min<Platform>`
        /// values AND-combine inside the SQL — a sample must satisfy
        /// every requested minimum to pass. The legacy `(platform,
        /// minVersion)` pair (#233) is still honoured for `searchFiles`
        /// callers that haven't migrated, but new MCP / CLI surfaces
        /// use the 5-field shape.
        public func search(_ query: Sample.Search.Query) async throws -> Sample.Search.Result {
            let projects = try await database.searchProjects(
                query: query.text,
                framework: query.framework,
                limit: query.limit,
                minIOS: query.minIOS,
                minMacOS: query.minMacOS,
                minTvOS: query.minTvOS,
                minWatchOS: query.minWatchOS,
                minVisionOS: query.minVisionOS
            )

            var files: [Sample.Index.FileSearchResult] = []
            if query.searchFiles {
                files = try await database.searchFiles(
                    query: query.text,
                    projectId: nil,
                    fileExtension: nil,
                    limit: query.limit,
                    platform: query.platform,
                    minVersion: query.minVersion
                )

                // #837 read-side wiring — boost files whose file_symbols
                // row matches the query in name / attributes / conformances /
                // signature / generic_constraints. Same shape as
                // Search.Index.search's apple-docs symbol-boost path: rows
                // returned from FTS5 keep their native BM25 rank (lower =
                // better); we multiply the rank of symbol-matched rows by
                // 3.0 to make it more negative (= better). RRF picks up
                // the reordering at the SmartQuery layer.
                let symbolMatchedKeys = try await database.searchSymbolsForFiles(query: query.text, limit: 500)
                if !symbolMatchedKeys.isEmpty {
                    files = files.map { file in
                        let key = "\(file.projectId)|\(file.path)"
                        guard symbolMatchedKeys.contains(key) else { return file }
                        return Sample.Index.FileSearchResult(
                            projectId: file.projectId,
                            path: file.path,
                            filename: file.filename,
                            snippet: file.snippet,
                            rank: file.rank * 3.0
                        )
                    }
                    files.sort { $0.rank < $1.rank }
                }
            }

            return Sample.Search.Result(projects: projects, files: files)
        }

        /// Simple text search
        public func search(text: String, limit: Int = Shared.Constants.Limit.defaultSearchLimit) async throws -> Sample.Search.Result {
            try await search(Sample.Search.Query(text: text, limit: limit))
        }

        /// Search within a specific framework
        public func search(text: String, framework: String, limit: Int = Shared.Constants.Limit.defaultSearchLimit) async throws -> Sample.Search.Result {
            try await search(Sample.Search.Query(text: text, framework: framework, limit: limit))
        }

        // MARK: - Project Access

        /// Get a project by ID
        public func getProject(id: String) async throws -> Sample.Index.Project? {
            try await database.getProject(id: id)
        }

        /// List all projects
        public func listProjects(framework: String? = nil, limit: Int = 50) async throws -> [Sample.Index.Project] {
            try await database.listProjects(framework: framework, limit: limit)
        }

        /// Get total project count
        public func projectCount() async throws -> Int {
            try await database.projectCount()
        }

        // MARK: - File Access

        /// Get a file by project ID and path
        public func getFile(projectId: String, path: String) async throws -> Sample.Index.File? {
            try await database.getFile(projectId: projectId, path: path)
        }

        /// List files in a project
        public func listFiles(projectId: String, folder: String? = nil) async throws -> [Sample.Index.File] {
            try await database.listFiles(projectId: projectId, folder: folder)
        }

        /// Get total file count
        public func fileCount() async throws -> Int {
            try await database.fileCount()
        }

        // MARK: - Lifecycle

        /// Disconnect from the database
        public func disconnect() async {
            await database.disconnect()
        }
    }
}
