"""Core linter engine — checks Kotlin/Java source against the SDK JSON reference.

Checks:
  OPTIONAL_NO_CALL   — Bare property access on Optional<T> field
  WRONG_TYPE         — String literal where Duration/Instant expected
  MISSING_ARGS       — Service method called with too few args
  UNKNOWN_TYPE       — Import doesn't exist in SDK
  MISSING_IMPORT     — SDK type used but not imported
  LISTOF_VARARGS     — listOf() wrapper on varargs method
  STRING_ENUM        — String literal where Known enum expected

Supports inline suppression: // sdkspindle-ignore RULE_CODE
"""

import re
import json
import sys
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import Optional, Dict, List


@dataclass
class Issue:
    file: str
    line: int
    severity: str  # ERROR, WARN
    code: str
    message: str
    fix: Optional[dict] = None  # {"type": "replace", "old": ..., "new": ...}


def load_sdk(path: str) -> dict:
    with open(path) as f:
        return json.load(f)


def parse_source(filepath: str):
    """Read source, mask strings/comments, return (masked_lines, raw_lines, imports)."""
    with open(filepath, "r", encoding="utf-8") as f:
        raw = f.read()
    masked = raw
    # Mask triple-quoted strings (embedded Lua, prose, etc.)
    for m in re.finditer(r'"""(.*?)"""', raw, re.DOTALL):
        replacement = re.sub(r'[^\n]', ' ', m.group(1))
        masked = masked[:m.start(1)] + replacement + masked[m.end(1):]
    # Mask // line comments (but preserve sdkspindle-ignore directives)
    def mask_comment(m):
        if 'sdkspindle-ignore' in m.group(0):
            return m.group(0)  # keep suppression comments
        return ' ' * len(m.group(0))
    masked = re.sub(r'//[^\n]*', mask_comment, masked)
    # Mask /* block comments */
    masked = re.sub(r'/\*.*?\*/', lambda m: re.sub(r'[^\n]', ' ', m.group(0)), masked, flags=re.DOTALL)

    masked_lines = [line + "\n" for line in masked.split("\n")]
    raw_lines = [line + "\n" for line in raw.split("\n")]

    imports = {}
    for line in masked_lines:
        m = re.match(r'import\s+com\.google\.genai\.types\.(\w+)', line)
        if m:
            imports[m.group(1)] = f"com.google.genai.types.{m.group(1)}"
        m = re.match(r'import\s+com\.google\.genai\.(\w+)', line)
        if m and m.group(1) != "types":
            imports[m.group(1)] = f"com.google.genai.{m.group(1)}"
        m = re.match(r'import\s+(java\.time\.\w+)', line)
        if m:
            imports[m.group(1).split(".")[-1]] = m.group(1)

    return masked_lines, raw_lines, imports


def is_suppressed(raw_lines: list, line_idx: int, code: str) -> bool:
    """Check if a line has // sdkspindle-ignore RULE_CODE."""
    if line_idx < 0 or line_idx >= len(raw_lines):
        return False
    line = raw_lines[line_idx]
    if 'sdkspindle-ignore' in line:
        # Check if the specific code is suppressed, or if it's a blanket ignore
        suppress_match = re.search(r'sdkspindle-ignore\s+(\S+)', line)
        if suppress_match:
            codes = suppress_match.group(1).split(',')
            return code in codes or '*' in codes
        return True  # bare sdkspindle-ignore suppresses all
    # Also check previous line for standalone suppression comment
    if line_idx > 0:
        prev = raw_lines[line_idx - 1].strip()
        if prev.startswith('// sdkspindle-ignore'):
            suppress_match = re.search(r'sdkspindle-ignore\s+(\S+)', prev)
            if suppress_match:
                codes = suppress_match.group(1).split(',')
                return code in codes or '*' in codes
            return True
    return False


def get_severity(code: str, rule_overrides: dict) -> str:
    """Get severity from config overrides, or default."""
    defaults = {
        "OPTIONAL_NO_CALL": "ERROR", "WRONG_TYPE": "ERROR", "MISSING_ARGS": "ERROR",
        "UNKNOWN_TYPE": "ERROR", "MISSING_IMPORT": "ERROR",
        "LISTOF_VARARGS": "WARN", "STRING_ENUM": "WARN",
    }
    override = rule_overrides.get(code, "").upper()
    if override == "OFF":
        return "OFF"
    if override in ("ERROR", "WARN"):
        return override
    return defaults.get(code, "ERROR")


# ── Individual checks ───────────────────────────────────────────────────────

def check_unknown_imports(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("UNKNOWN_TYPE", rule_overrides)
    if sev == "OFF": return
    all_sdk_types = set(sdk.get("types", {}).keys()) | set(sdk.get("other", {}).keys())
    known_non_types = {"Chat", "Client", "ResponseStream", "Pager", "AsyncModels", "AsyncCaches"}
    for i, line in enumerate(lines):
        m = re.match(r'import\s+com\.google\.genai\.types\.(\w+)', line)
        if m:
            cls = m.group(1)
            if cls not in all_sdk_types and cls not in known_non_types:
                if not is_suppressed(raw_lines, i, "UNKNOWN_TYPE"):
                    issues.append(Issue(filepath, i+1, sev, "UNKNOWN_TYPE",
                        f"'{cls}' not found in SDK. Check spelling or SDK version."))


def check_optional_access(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("OPTIONAL_NO_CALL", rule_overrides)
    if sev == "OFF": return
    types_db = sdk.get("types", {})
    optional_fields = {}
    for type_name in imports:
        if type_name not in types_db: continue
        for fld in types_db[type_name].get("fields", []):
            if fld.get("optional"):
                optional_fields[fld["name"]] = (type_name, fld["type"])
    for i, line in enumerate(lines):
        for fname, (type_name, java_type) in optional_fields.items():
            for match in re.finditer(rf'\.{fname}(?!\w|\()', line):
                pre = line[:match.start()]
                if pre.count('"') % 2 == 1: continue
                if is_suppressed(raw_lines, i, "OPTIONAL_NO_CALL"): continue
                issues.append(Issue(filepath, i+1, sev, "OPTIONAL_NO_CALL",
                    f"'{fname}' on {type_name} returns {java_type} \u2014 use .{fname}().orElse(...)",
                    fix={"type": "regex_replace", "pattern": rf'\.{fname}(?!\w|\()',
                         "replacement": f".{fname}().orElse(\"\")", "line": i}))


def check_builder_param_types(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("WRONG_TYPE", rule_overrides)
    if sev == "OFF": return
    types_db = sdk.get("types", {})
    builder_methods = {}
    for type_name in imports:
        if type_name not in types_db: continue
        for bm in types_db[type_name].get("builderMethods", []):
            key = bm["name"]
            if key not in builder_methods: builder_methods[key] = []
            builder_methods[key].append(bm["paramType"])
    for i, line in enumerate(lines):
        for method_name, param_types in builder_methods.items():
            for match in re.finditer(rf'\.{method_name}\(', line):
                after = line[match.end():]
                if "Duration" in param_types and "String" not in param_types:
                    if re.match(r'\s*"[^"]*"', after):
                        if is_suppressed(raw_lines, i, "WRONG_TYPE"): continue
                        issues.append(Issue(filepath, i+1, sev, "WRONG_TYPE",
                            f".{method_name}() expects {'/'.join(set(param_types))}, got String literal"))
                if "Instant" in param_types and "String" not in param_types:
                    if re.match(r'\s*"[^"]*"', after):
                        if is_suppressed(raw_lines, i, "WRONG_TYPE"): continue
                        issues.append(Issue(filepath, i+1, sev, "WRONG_TYPE",
                            f".{method_name}() expects {'/'.join(set(param_types))}, got String literal"))
                if "Duration" in param_types:
                    if re.match(r'\s*\d+\s*[,)]', after):
                        if is_suppressed(raw_lines, i, "WRONG_TYPE"): continue
                        issues.append(Issue(filepath, i+1, sev, "WRONG_TYPE",
                            f".{method_name}() expects Duration, got raw number"))


def check_service_signatures(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("MISSING_ARGS", rule_overrides)
    if sev == "OFF": return
    services_db = sdk.get("services", {})
    full_source = "".join(lines)
    for svc_name, svc_info in services_db.items():
        svc_lower = svc_name[0].lower() + svc_name[1:]
        for method in svc_info.get("methods", []):
            mname = method["name"]
            params = method.get("params", [])
            for match in re.finditer(rf'{svc_lower}\.{mname}\(', full_source):
                line_num = full_source[:match.start()].count('\n')
                after = full_source[match.end():]
                depth, arg_count, has_content, j = 1, 0, False, 0
                while j < len(after) and depth > 0:
                    ch = after[j]
                    if ch == '(': depth += 1
                    elif ch == ')':
                        depth -= 1
                        if depth == 0: break
                    elif ch == ',' and depth == 1: arg_count += 1
                    if ch not in (' ', '\t', '\n', '\r'): has_content = True
                    j += 1
                if has_content: arg_count += 1
                if arg_count == 0 and len(params) > 0:
                    if is_suppressed(raw_lines, line_num, "MISSING_ARGS"): continue
                    expected = ", ".join(p["type"] for p in params)
                    issues.append(Issue(filepath, line_num+1, sev, "MISSING_ARGS",
                        f"{svc_lower}.{mname}() requires ({expected}) \u2014 called with no args"))


def check_missing_imports(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("MISSING_IMPORT", rule_overrides)
    if sev == "OFF": return
    types_db = sdk.get("types", {})
    all_type_names = set(types_db.keys())
    imported_types = set(k for k in imports if k in all_type_names)
    for i, line in enumerate(lines):
        if line.strip().startswith("import "): continue
        for m in re.finditer(r'\b([A-Z]\w+)\.(?:builder|fromParts|fromText|fromJson|Known)\b', line):
            type_name = m.group(1)
            if type_name in all_type_names and type_name not in imported_types:
                if is_suppressed(raw_lines, i, "MISSING_IMPORT"): continue
                issues.append(Issue(filepath, i+1, sev, "MISSING_IMPORT",
                    f"'{type_name}' used but not imported. Add: import com.google.genai.types.{type_name}",
                    fix={"type": "add_import", "import": f"import com.google.genai.types.{type_name}"}))


def check_listof_on_varargs(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("LISTOF_VARARGS", rule_overrides)
    if sev == "OFF": return
    types_db = sdk.get("types", {})
    vararg_methods = set()
    for type_name in imports:
        if type_name not in types_db: continue
        for bm in types_db[type_name].get("builderMethods", []):
            if bm.get("varargs"): vararg_methods.add(bm["name"])
    for i, line in enumerate(lines):
        for mname in vararg_methods:
            if re.search(rf'\.{mname}\(\s*listOf\(', line):
                if is_suppressed(raw_lines, i, "LISTOF_VARARGS"): continue
                issues.append(Issue(filepath, i+1, sev, "LISTOF_VARARGS",
                    f".{mname}() accepts varargs \u2014 listOf() is unnecessary"))


def check_enum_usage(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("STRING_ENUM", rule_overrides)
    if sev == "OFF": return
    types_db = sdk.get("types", {})
    other_db = sdk.get("other", {})
    enum_values = {}
    for db in [types_db, other_db]:
        for type_name, info in db.items():
            vals = info.get("enumValues", [])
            if vals: enum_values[type_name] = set(vals)
    for type_name in imports:
        if type_name not in types_db: continue
        for bm in types_db[type_name].get("builderMethods", []):
            param = bm["paramType"]
            for etype in enum_values:
                if param == etype or param.endswith(f".{etype}"):
                    for i, line in enumerate(lines):
                        m = re.search(rf'\.{bm["name"]}\(\s*"(\w+)"', line)
                        if m:
                            if is_suppressed(raw_lines, i, "STRING_ENUM"): continue
                            val = m.group(1)
                            issues.append(Issue(filepath, i+1, sev, "STRING_ENUM",
                                f".{bm['name']}() expects {etype}.Known enum, got \"{val}\""))


def check_java_time_imports(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("MISSING_IMPORT", rule_overrides)
    if sev == "OFF": return
    types_db = sdk.get("types", {})
    needs = {"Duration": False, "Instant": False}
    for type_name in imports:
        if type_name not in types_db: continue
        for bm in types_db[type_name].get("builderMethods", []):
            if bm["paramType"] in needs: needs[bm["paramType"]] = True
    source_text = "".join(lines)
    for cls, needed in needs.items():
        if needed and f"{cls}." in source_text and cls not in imports:
            issues.append(Issue(filepath, 0, sev, "MISSING_IMPORT",
                f"{cls} used but not imported. Add: import java.time.{cls}",
                fix={"type": "add_import", "import": f"import java.time.{cls}"}))


def check_service_return_fields(lines, raw_lines, imports, sdk, issues, filepath, rule_overrides):
    sev = get_severity("OPTIONAL_NO_CALL", rule_overrides)
    if sev == "OFF": return
    services_db = sdk.get("services", {})
    types_db = sdk.get("types", {})
    full_source = "".join(lines)
    return_types = {}
    for svc_name, svc_info in services_db.items():
        svc_lower = svc_name[0].lower() + svc_name[1:]
        for method in svc_info.get("methods", []):
            ret = method.get("returnType", "")
            inner = re.match(r'(?:Pager|List)<(\w+)>', ret)
            actual = inner.group(1) if inner else ret
            return_types[f"{svc_lower}.{method['name']}"] = actual
    for call_key, ret_type in return_types.items():
        if ret_type not in types_db: continue
        svc_part = call_key.split(".")[0]
        if svc_part not in full_source: continue
        for fld in types_db[ret_type].get("fields", []):
            if not fld.get("optional"): continue
            fname = fld["name"]
            for i, line in enumerate(lines):
                for match in re.finditer(rf'\.{fname}(?!\w|\()', line):
                    pre = line[:match.start()]
                    if pre.count('"') % 2 == 1: continue
                    if is_suppressed(raw_lines, i, "OPTIONAL_NO_CALL"): continue
                    issues.append(Issue(filepath, i+1, sev, "OPTIONAL_NO_CALL",
                        f"'{fname}' on {ret_type} (from {call_key}) returns {fld['type']} \u2014 use .{fname}().orElse(...)"))


# ── Main entry point ────────────────────────────────────────────────────────

def check_file(filepath: str, sdk: dict, rule_overrides: dict = None) -> list:
    """Run all checks on a single file. Returns list of Issue."""
    if rule_overrides is None: rule_overrides = {}
    issues = []
    masked_lines, raw_lines, imports = parse_source(filepath)
    sdk_imports = {k: v for k, v in imports.items() if v.startswith("com.google.genai")}
    if not sdk_imports:
        return issues
    check_unknown_imports(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    check_optional_access(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    check_builder_param_types(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    check_service_signatures(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    check_missing_imports(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    check_listof_on_varargs(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    check_enum_usage(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    check_java_time_imports(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    check_service_return_fields(masked_lines, raw_lines, imports, sdk, issues, filepath, rule_overrides)
    return issues
