"""
Code generator — generates validation code for target platforms.

Supports: Snowflake (JS UDF), Salesforce (Apex), JavaScript.
Covers: regex, min, max, range, not_empty, min_length, max_length, date_format, unique.

Rule types not yet implemented emit a // TODO comment rather than being silently dropped.
This is a "push-down" feature: deploy OpenDQV rules directly into source systems
as a complement to the centralized API validation.
"""

from datetime import datetime, timezone
from typing import List, Union
from .rule_parser import Rule


def generate_code(
    rules: Union[List[Rule], List[dict]],
    target: str,
    contract_name: str = "",
    contract_version: str = "",
) -> str:
    """Generate platform-specific validation code from a list of rules.

    Args:
        rules: List of Rule objects or raw rule dicts.
        target: Output platform. Supported values: "snowflake" (JS UDF),
                "salesforce" (Apex trigger), "js" (plain JavaScript).
        contract_name: Optional contract name to embed in the generated header.
        contract_version: Optional contract version to embed in the generated header.

    Returns:
        A string containing the generated source code ready to deploy.

    Raises:
        ValueError: If target is not one of the supported platform strings.
    """
    rule_dicts = []
    for r in rules:
        if isinstance(r, Rule):
            rule_dicts.append(r.model_dump(by_alias=False))
        else:
            rule_dicts.append(r)

    if target == "snowflake":
        return _generate_snowflake(rule_dicts, contract_name, contract_version)
    elif target == "salesforce":
        return _generate_salesforce(rule_dicts, contract_name, contract_version)
    elif target == "js":
        return _generate_js(rule_dicts, contract_name, contract_version)
    else:
        raise ValueError(f"Unsupported target: {target}")


def _generate_snowflake(rules: list, contract_name: str = "", contract_version: str = "") -> str:
    header = ""
    if contract_name:
        ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        header = (
            f"-- Generated by OpenDQV — push-down validation snapshot\n"
            f"-- Contract: {contract_name} v{contract_version} | Generated: {ts}\n"
            f"-- SYNC REMINDER: This UDF is a snapshot. Re-run if the contract has been updated:\n"
            f"--   opendqv generate {contract_name} snowflake\n"
            f"-- For live governance, use the Snowflake External Function integration.\n"
            f"\n"
        )
    code = header + (
        "CREATE OR REPLACE FUNCTION opendqv_validate(data ARRAY)\n"
        "RETURNS ARRAY\n"
        "LANGUAGE JAVASCRIPT\n"
        "AS $$\n"
        "let results = [];\n"
        "for (let row of data) {\n"
        "    let errors = [];\n"
    )
    age_checked = set()
    for rule in rules:
        code += _js_rule_check(rule, indent="    ", age_checked=age_checked)
    code += (
        "    results.push({valid: errors.length === 0, errors: errors});\n"
        "}\n"
        "return results;\n"
        "$$;\n"
    )
    return code


def _escape_single(s: str) -> str:
    """Escape single quotes for JS/Apex string literals."""
    return s.replace("\\", "\\\\").replace("'", "\\'")


def _escape_pattern(p: str) -> str:
    """Escape a regex pattern for safe embedding in a JS/Apex string literal."""
    return p.replace("\\", "\\\\").replace("'", "\\'")


def _generate_salesforce(rules: list, contract_name: str = "", contract_version: str = "") -> str:
    header = ""
    if contract_name:
        ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        header = (
            f"// Generated by OpenDQV — push-down validation snapshot\n"
            f"// Contract: {contract_name} v{contract_version} | Generated: {ts}\n"
            f"// SYNC REMINDER: This class is a snapshot of the contract rules at generation\n"
            f"// time. Re-run if the contract has been updated:\n"
            f"//   opendqv generate {contract_name} salesforce\n"
            f"// For live governance (always in sync), see the HTTP callout integration.\n"
            f"\n"
        )
    code = header + (
        "public class OpenDQVValidator {\n"
        "    public static List<Map<String, Object>> validate(List<Map<String, Object>> data) {\n"
        "        List<Map<String, Object>> results = new List<Map<String, Object>>();\n"
        "        for (Map<String, Object> row : data) {\n"
        "            List<String> errors = new List<String>();\n"
        "            List<String> warnings = new List<String>();\n"
    )
    age_checked_fields = set()
    for rule in rules:
        field = rule["field"]
        error = _escape_single(rule.get("error_message", f"Failed rule {rule['name']}"))
        rtype = rule["type"]
        severity = rule.get("severity", "error")
        target_list = "errors" if severity == "error" else "warnings"

        if rtype == "regex" and rule.get("pattern"):
            safe_pattern = _escape_pattern(rule['pattern'])
            code += f"            if (row.get('{field}') != null && !Pattern.matches('{safe_pattern}', (String)row.get('{field}'))) {target_list}.add('{error}');\n"
        elif rtype == "min" and rule.get("min_value") is not None:
            code += f"            if (row.get('{field}') != null && (Decimal)row.get('{field}') < {rule['min_value']}) {target_list}.add('{error}');\n"
        elif rtype == "max" and rule.get("max_value") is not None:
            code += f"            if (row.get('{field}') != null && (Decimal)row.get('{field}') > {rule['max_value']}) {target_list}.add('{error}');\n"
        elif rtype == "range" and rule.get("min_value") is not None and rule.get("max_value") is not None:
            code += f"            if (row.get('{field}') != null) {{ Decimal val_{field} = (Decimal)row.get('{field}'); if (val_{field} < {rule['min_value']} || val_{field} > {rule['max_value']}) {target_list}.add('{error}'); }}\n"
        elif rtype == "not_empty":
            code += f"            if (row.get('{field}') == null || String.valueOf(row.get('{field}')).trim() == '') {target_list}.add('{error}');\n"
        elif rtype == "min_length" and rule.get("min_length") is not None:
            code += f"            if (((String)row.get('{field}')).length() < {rule['min_length']}) {target_list}.add('{error}');\n"
        elif rtype == "date_format":
            code += f"            if (row.get('{field}') != null) {{ try {{ Date.valueOf((String)row.get('{field}')); }} catch (Exception e) {{ {target_list}.add('{error}'); }} }}\n"
        elif rtype == "unique":
            code += f"            // Unique check for '{field}' requires full dataset\n"
        elif rtype in ("required_if", "lookup", "compare", "date_diff", "checksum",
                       "cross_field_range", "field_sum", "forbidden_if", "conditional_value",
                       "ratio_check", "geospatial_bounds", "conditional_lookup", "allowed_values"):
            code += f"            // NOTE: rule '{rule['name']}' (type='{rtype}') requires API validation — not implementable as push-down code\n"
        else:
            code += f"            // TODO: rule '{rule['name']}' (type='{rtype}') not implemented for this target\n"

        if (rule.get("min_age") is not None or rule.get("max_age") is not None) and field not in age_checked_fields:
            age_checked_fields.add(field)
            code += f"            if (row.get('{field}') != null) {{\n"
            code += f"                Date dob_{field} = Date.valueOf((String)row.get('{field}'));\n"
            code += f"                Integer age_{field} = dob_{field}.daysBetween(Date.today()) / 365;\n"
            if rule.get("min_age") is not None:
                code += f"                if (age_{field} < {rule['min_age']}) {target_list}.add('{error}');\n"
            if rule.get("max_age") is not None:
                code += f"                if (age_{field} > {rule['max_age']}) {target_list}.add('{error}');\n"
            code += "            }\n"

    code += (
        "            results.add(new Map<String, Object>{ 'valid' => errors.isEmpty(), 'errors' => errors, 'warnings' => warnings });\n"
        "        }\n"
        "        return results;\n"
        "    }\n"
        "}\n"
    )
    return code


def _generate_js(rules: list, contract_name: str = "", contract_version: str = "") -> str:
    header = ""
    if contract_name:
        ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        header = (
            f"// Generated by OpenDQV — push-down validation snapshot\n"
            f"// Contract: {contract_name} v{contract_version} | Generated: {ts}\n"
            f"// SYNC REMINDER: This function is a snapshot. Re-run if the contract has been updated:\n"
            f"//   opendqv generate {contract_name} js\n"
            f"// For live governance, use the OpenDQV API integration.\n"
            f"\n"
        )
    code = header + (
        "function opendqvValidate(data) {\n"
        "    let results = [];\n"
        "    for (let row of data) {\n"
        "        let errors = [];\n"
    )
    age_checked = set()
    for rule in rules:
        code += _js_rule_check(rule, indent="        ", age_checked=age_checked)
    code += (
        "        results.push({valid: errors.length === 0, errors: errors});\n"
        "    }\n"
        "    return results;\n"
        "}\n"
    )
    return code


def _js_rule_check(rule: dict, indent: str = "    ", age_checked: set = None) -> str:
    """Generate a JS rule check snippet (shared by Snowflake and plain JS)."""
    if age_checked is None:
        age_checked = set()
    field = rule["field"]
    error = _escape_single(rule.get("error_message", f"Failed rule {rule['name']}"))
    rtype = rule["type"]
    snippet = ""

    if rtype == "regex" and rule.get("pattern"):
        safe_pattern = _escape_pattern(rule['pattern'])
        snippet += f"{indent}if (!new RegExp('{safe_pattern}').test(row['{field}'])) errors.push('{error}');\n"
    elif rtype == "min" and rule.get("min_value") is not None:
        snippet += f"{indent}if (parseFloat(row['{field}']) < {rule['min_value']}) errors.push('{error}');\n"
    elif rtype == "max" and rule.get("max_value") is not None:
        snippet += f"{indent}if (parseFloat(row['{field}']) > {rule['max_value']}) errors.push('{error}');\n"
    elif rtype == "range" and rule.get("min_value") is not None and rule.get("max_value") is not None:
        snippet += (
            f"{indent}if (parseFloat(row['{field}']) < {rule['min_value']} || "
            f"parseFloat(row['{field}']) > {rule['max_value']}) errors.push('{error}');\n"
        )
    elif rtype == "not_empty":
        snippet += f"{indent}if (!row['{field}'] || row['{field}'].toString().trim() === '') errors.push('{error}');\n"
    elif rtype == "min_length" and rule.get("min_length") is not None:
        snippet += f"{indent}if (!row['{field}'] || row['{field}'].toString().length < {rule['min_length']}) errors.push('{error}');\n"
    elif rtype == "max_length" and rule.get("max_length") is not None:
        snippet += f"{indent}if (row['{field}'] && row['{field}'].toString().length > {rule['max_length']}) errors.push('{error}');\n"
    elif rtype == "date_format":
        snippet += f"{indent}if (isNaN(Date.parse(row['{field}']))) errors.push('{error}');\n"
    elif rtype == "unique":
        snippet += f"{indent}// Unique check for '{field}' requires full dataset — implement via pre-scan\n"
    elif rtype not in ("required_if", "lookup", "compare", "date_diff", "checksum",
                       "cross_field_range", "field_sum", "forbidden_if", "conditional_value",
                       "ratio_check", "geospatial_bounds", "conditional_lookup", "allowed_values"):
        # Unknown rule type — emit explicit TODO rather than silently dropping
        snippet += f"{indent}// TODO: rule '{rule['name']}' (type='{rtype}') not implemented for this target\n"
    else:
        # Known unsupported rule types — these require full-dataset or API context
        snippet += f"{indent}// NOTE: rule '{rule['name']}' (type='{rtype}') requires API validation — not implementable as push-down code\n"

    # Age check — appended after type check for rules with min_age/max_age (once per field)
    if (rule.get("min_age") is not None or rule.get("max_age") is not None) and field not in age_checked:
        age_checked.add(field)
        snippet += f"{indent}if (row['{field}']) {{\n"
        snippet += f"{indent}    let dob = new Date(row['{field}']);\n"
        snippet += f"{indent}    let age = Math.floor((Date.now() - dob.getTime()) / (365.25 * 24 * 60 * 60 * 1000));\n"
        if rule.get("min_age") is not None:
            snippet += f"{indent}    if (age < {rule['min_age']}) errors.push('{error}');\n"
        if rule.get("max_age") is not None:
            snippet += f"{indent}    if (age > {rule['max_age']}) errors.push('{error}');\n"
        snippet += f"{indent}}}\n"

    return snippet
