001package gudusoft.gsqlparser.ir.builder.mssql;
002
003import gudusoft.gsqlparser.ir.bound.*;
004import gudusoft.gsqlparser.ir.common.*;
005
006import java.util.*;
007
008/**
009 * Resolves T-SQL {@link BoundRoutineRef} instances against the full
010 * {@link BoundProgram#getRoutineIndex()}.
011 * <p>
012 * Uses a four-tier candidate scoring strategy:
013 * <ol>
014 *   <li>Tier 1: Qualified name exact match ({@code schema.name/KIND(argCount)})</li>
015 *   <li>Tier 2: Unqualified name + default schema ({@code dbo}) inference</li>
016 *   <li>Tier 3: Name + argCount → single candidate (with default param tolerance)</li>
017 *   <li>Tier 4: Multiple candidates → AMBIGUOUS</li>
018 * </ol>
019 * <p>
020 * Refs marked with {@code externalDependency=true} are skipped.
021 * <p>
022 * This resolver is designed to run AFTER merging partial BoundPrograms
023 * from multiple scripts, so that the routineIndex is complete.
024 */
025public final class MssqlRoutineRefResolver {
026
027    private MssqlRoutineRefResolver() {}
028
029    /** KIND codes to try in order. */
030    private static final String[] KIND_CODES = {"P", "SF", "IF", "TF", "F", "T", "TR", "A"};
031
032    /**
033     * Resolves all routine references in the program.
034     * Modifies the program's routineRefs list in place (via replaceRoutineRefs).
035     */
036    public static void resolve(BoundProgram program) {
037        if (program == null) return;
038
039        // Build case-insensitive lookup: uppercase routineId → symbol
040        Map<String, BoundRoutineSymbol> upperIndex = new LinkedHashMap<String, BoundRoutineSymbol>();
041        for (Map.Entry<String, BoundRoutineSymbol> entry : program.getRoutineIndex().entrySet()) {
042            upperIndex.put(entry.getKey().toUpperCase(), entry.getValue());
043        }
044
045        // Build name-based index: uppercase name → list of symbols
046        Map<String, List<BoundRoutineSymbol>> nameIndex =
047                new LinkedHashMap<String, List<BoundRoutineSymbol>>();
048        for (BoundRoutineSymbol sym : program.getRoutineIndex().values()) {
049            String nameKey = sym.getRoutineName().toUpperCase();
050            List<BoundRoutineSymbol> list = nameIndex.get(nameKey);
051            if (list == null) {
052                list = new ArrayList<BoundRoutineSymbol>();
053                nameIndex.put(nameKey, list);
054            }
055            list.add(sym);
056        }
057
058        List<BoundRoutineRef> resolved = new ArrayList<BoundRoutineRef>();
059
060        for (BoundRoutineRef ref : program.getAllRoutineRefs()) {
061            // Skip already resolved or external deps
062            if (ref.getBindingStatus() == EBindingStatus.EXACT) {
063                resolved.add(ref);
064                continue;
065            }
066            if (Boolean.TRUE.equals(ref.getProperty("externalDependency"))) {
067                resolved.add(ref);
068                continue;
069            }
070
071            BoundRoutineRef resolvedRef = resolveRef(ref, upperIndex, nameIndex);
072            resolved.add(resolvedRef);
073        }
074
075        program.replaceRoutineRefs(resolved);
076    }
077
078    private static BoundRoutineRef resolveRef(
079            BoundRoutineRef ref,
080            Map<String, BoundRoutineSymbol> upperIndex,
081            Map<String, List<BoundRoutineSymbol>> nameIndex) {
082
083        String rawText = ref.getOriginalText();
084        MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawText);
085        String objectName = normalized.getObject().toUpperCase();
086        String schemaName = normalized.getSchema();
087        Integer argCount = ref.getProperty("argCount");
088        int args = argCount != null ? argCount : 0;
089
090        // Tier 1: Qualified exact match (schema.name/KIND(argCount))
091        if (schemaName != null && !schemaName.isEmpty()) {
092            String schemaUpper = schemaName.toUpperCase();
093            for (String kind : KIND_CODES) {
094                String candidateId = (schemaUpper + "." + objectName + "/" + kind + "(" + args + ")").toUpperCase();
095                BoundRoutineSymbol found = upperIndex.get(candidateId);
096                if (found != null) {
097                    return ref.withResolvedRoutine(found, EBindingStatus.EXACT, null,
098                            new Evidence(EvidenceKind.STATIC_RESOLVED,
099                                    "Tier1: qualified match " + found.getRoutineId()));
100                }
101            }
102            // Try with default param tolerance
103            for (String kind : KIND_CODES) {
104                for (Map.Entry<String, BoundRoutineSymbol> entry : upperIndex.entrySet()) {
105                    BoundRoutineSymbol sym = entry.getValue();
106                    if (sym.getRoutineName().toUpperCase().equals(objectName)
107                            && sym.getPackageName() != null
108                            && sym.getPackageName().toUpperCase().equals(schemaUpper.toUpperCase())
109                            && sym.getParameters().size() >= args) {
110                        return ref.withResolvedRoutine(sym, EBindingStatus.EXACT, null,
111                                new Evidence(EvidenceKind.STATIC_RESOLVED,
112                                        "Tier1: qualified match with default params " + sym.getRoutineId()));
113                    }
114                }
115            }
116        }
117
118        // Tier 2: Unqualified name + default schema 'dbo'
119        if (schemaName == null || schemaName.isEmpty()) {
120            for (String kind : KIND_CODES) {
121                String candidateId = ("DBO." + objectName + "/" + kind + "(" + args + ")").toUpperCase();
122                BoundRoutineSymbol found = upperIndex.get(candidateId);
123                if (found != null) {
124                    return ref.withResolvedRoutine(found, EBindingStatus.EXACT, null,
125                            new Evidence(EvidenceKind.STATIC_RESOLVED,
126                                    "Tier2: unqualified + default schema dbo"));
127                }
128            }
129        }
130
131        // Tier 3: Name + argCount across all schemas
132        List<BoundRoutineSymbol> candidates = nameIndex.get(objectName);
133        if (candidates != null && !candidates.isEmpty()) {
134            // Exact arg count match
135            List<BoundRoutineSymbol> exactArgMatch = new ArrayList<BoundRoutineSymbol>();
136            for (BoundRoutineSymbol sym : candidates) {
137                if (sym.getParameters().size() == args) {
138                    exactArgMatch.add(sym);
139                }
140            }
141            if (exactArgMatch.size() == 1) {
142                return ref.withResolvedRoutine(exactArgMatch.get(0), EBindingStatus.EXACT, null,
143                        new Evidence(EvidenceKind.STATIC_RESOLVED,
144                                "Tier3: name+argCount single match"));
145            }
146
147            // Tier 3b: Default parameter tolerance (paramCount > argCount)
148            if (exactArgMatch.isEmpty()) {
149                List<BoundRoutineSymbol> tolerantMatch = new ArrayList<BoundRoutineSymbol>();
150                for (BoundRoutineSymbol sym : candidates) {
151                    if (sym.getParameters().size() >= args) {
152                        tolerantMatch.add(sym);
153                    }
154                }
155                if (tolerantMatch.size() == 1) {
156                    return ref.withResolvedRoutine(tolerantMatch.get(0), EBindingStatus.EXACT, null,
157                            new Evidence(EvidenceKind.STATIC_RESOLVED,
158                                    "Tier3b: name match with default params"));
159                }
160            }
161
162            // Tier 4: Multiple candidates → AMBIGUOUS
163            if (exactArgMatch.size() > 1) {
164                StringBuilder sb = new StringBuilder("Tier4: ambiguous, ");
165                sb.append(exactArgMatch.size()).append(" candidates: [");
166                for (int i = 0; i < exactArgMatch.size(); i++) {
167                    if (i > 0) sb.append(", ");
168                    sb.append(exactArgMatch.get(i).getRoutineId());
169                }
170                sb.append("]");
171                return ref.withResolvedRoutine(null, EBindingStatus.AMBIGUOUS, exactArgMatch,
172                        new Evidence(EvidenceKind.AMBIGUOUS_NAME, sb.toString()));
173            }
174
175            // Single candidate by name alone (any arg count)
176            if (candidates.size() == 1) {
177                return ref.withResolvedRoutine(candidates.get(0), EBindingStatus.EXACT, null,
178                        new Evidence(EvidenceKind.HEURISTIC_MATCH,
179                                "Tier3: single name match (argCount relaxed)"));
180            }
181
182            // Multiple by name → ambiguous
183            if (candidates.size() > 1) {
184                StringBuilder sb = new StringBuilder("Tier4: ambiguous by name, ");
185                sb.append(candidates.size()).append(" candidates");
186                return ref.withResolvedRoutine(null, EBindingStatus.AMBIGUOUS, candidates,
187                        new Evidence(EvidenceKind.AMBIGUOUS_NAME, sb.toString()));
188            }
189        }
190
191        // No match → UNRESOLVED_SOFT
192        return ref;
193    }
194}