"""Azure Data Factory (ADF) submission validator and safe extraction.

Handles safe ZIP extraction with size/count limits and ADF format detection.
"""

import json
import zipfile
import tempfile
from pathlib import Path
from typing import Dict, Any, Optional, List
import shutil


# Extraction limits to prevent resource exhaustion
MAX_FILES = 200
MAX_TOTAL_SIZE = 50 * 1024 * 1024  # 50MB
MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB


class AdfValidationError(Exception):
    """Raised when ADF validation fails."""
    pass


def validate_adf_json(json_obj: Dict[str, Any]) -> bool:
    """Validate that the JSON structure looks like a plausible ADF document.

    Checks for:
    - ARM template with $schema containing 'arm' and resources of type 'Microsoft.DataFactory/factories'
    - OR pipeline JSON with 'name', 'properties', 'activities'
    - OR dataset JSON with 'name', 'properties'
    - OR linkedService JSON with 'name', 'properties'

    Args:
        json_obj: Parsed JSON object

    Returns:
        True if structure matches ADF patterns, False otherwise
    """
    if not isinstance(json_obj, dict):
        return False

    # Check for ARM template
    schema = json_obj.get("$schema", "").lower()
    if "arm" in schema:
        resources = json_obj.get("resources", [])
        if isinstance(resources, list):
            for res in resources:
                if isinstance(res, dict):
                    res_type = res.get("type", "").lower()
                    if "datafactory" in res_type:
                        return True

    # Check for pipeline
    if "properties" in json_obj and "activities" in json_obj.get("properties", {}):
        if "name" in json_obj:
            return True

    # Check for dataset
    if "properties" in json_obj and json_obj.get("type") == "Microsoft.DataFactory/factories/datasets":
        if "name" in json_obj:
            return True

    # Check for linked service
    if "properties" in json_obj and json_obj.get("type") == "Microsoft.DataFactory/factories/linkedServices":
        if "name" in json_obj:
            return True

    # Fallback: if it has name, properties, and seems structured, consider it valid
    if "name" in json_obj and "properties" in json_obj:
        return True

    return False


def extract_zip_safe(zip_path: str) -> str:
    """Safely extract a ZIP file with security limits.

    Enforcement:
    - Max 200 files
    - Max 50MB total
    - Max 10MB per file
    - Prevent path traversal (.. in paths)

    Args:
        zip_path: Path to ZIP file

    Returns:
        Path to temporary extraction directory

    Raises:
        AdfValidationError: If limits exceeded or path traversal detected
        FileNotFoundError: If ZIP file not found
    """
    zip_path_obj = Path(zip_path).resolve()

    if not zip_path_obj.exists():
        raise FileNotFoundError(f"ZIP file not found: {zip_path}")

    if not zipfile.is_zipfile(zip_path_obj):
        raise AdfValidationError(f"Invalid ZIP file: {zip_path}")

    with zipfile.ZipFile(zip_path_obj, "r") as z:
        files = z.filelist

        # Check file count
        if len(files) > MAX_FILES:
            raise AdfValidationError(
                f"ZIP contains {len(files)} files, exceeds limit of {MAX_FILES}"
            )

        # Check total size and individual file sizes + path traversal
        total_size = 0
        for file_info in files:
            # Check for path traversal
            if ".." in file_info.filename:
                raise AdfValidationError(
                    f"Suspicious path detected: {file_info.filename}"
                )

            # Check individual file size
            if file_info.file_size > MAX_FILE_SIZE:
                raise AdfValidationError(
                    f"File {file_info.filename} exceeds limit of {MAX_FILE_SIZE} bytes"
                )

            total_size += file_info.file_size

        # Check total size
        if total_size > MAX_TOTAL_SIZE:
            raise AdfValidationError(
                f"Total ZIP size {total_size} bytes exceeds limit of {MAX_TOTAL_SIZE} bytes"
            )

    # Safe to extract
    temp_dir = tempfile.mkdtemp(prefix="adf_submission_")
    try:
        with zipfile.ZipFile(zip_path_obj, "r") as z:
            z.extractall(temp_dir)
    except Exception as e:
        shutil.rmtree(temp_dir, ignore_errors=True)
        raise AdfValidationError(f"Failed to extract ZIP: {e}")

    return temp_dir


def find_adf_json_files(directory: str) -> List[Dict[str, Any]]:
    """Find and parse ADF JSON files from a directory.

    VALIDATION RULES:
    1. ZIP/folder may contain `.json` ADF artifacts and optional `.txt` companion notes
    2. Accepts:
       - Single .json file at root
       - ADF folder structure: pipeline/, dataset/, linkedService/, trigger/
       - ARM template (ARMTemplate*.json)
    
    Args:
        directory: Path to search (extracted ZIP directory)

    Returns:
        List of parsed JSON objects

    Raises:
        AdfValidationError: If validation fails (no JSON or unsupported files present)
    """
    dir_path = Path(directory).resolve()
    found_jsons: List[Dict[str, Any]] = []

    if not dir_path.exists() or not dir_path.is_dir():
        raise AdfValidationError(f"Directory not found: {directory}")

    # Step 1: Check all files - allow JSON + optional TXT only
    all_files = list(dir_path.rglob("*"))
    actual_files = [f for f in all_files if f.is_file()]
    
    # Count JSON files
    json_files = [f for f in actual_files if f.suffix.lower() == ".json"]
    txt_files = [f for f in actual_files if f.suffix.lower() == ".txt"]
    unsupported_files = [
        f for f in actual_files
        if f.suffix.lower() not in {".json", ".txt"}
    ]
    
    # STRICT RULE: Reject unsupported files
    if unsupported_files:
        non_json_names = [f.name for f in unsupported_files[:3]]
        raise AdfValidationError(
            f"ZIP contains non-JSON files: {', '.join(non_json_names)}.\n\n"
            "ADF projects may include only .json files and optional .txt notes.\n"
            "Please remove unsupported files and resubmit."
        )
    
    # STRICT RULE: Must have at least 1 JSON file
    if len(json_files) == 0:
        raise AdfValidationError(
            "ZIP contains no .json files.\n\n"
            "Please upload a ZIP with .json file(s) containing your ADF definition."
        )
    
    # Step 2: Detect ADF structure
    # Check for ARM template first (priority)
    arm_templates = [f for f in json_files if "armtemplate" in f.name.lower()]
    if arm_templates:
        for arm_file in arm_templates:
            try:
                content = arm_file.read_text(encoding="utf-8-sig")
                json_obj = json.loads(content)
                if validate_adf_json(json_obj):
                    found_jsons.append({
                        "filename": arm_file.name,
                        "path": str(arm_file),
                        "content": json_obj,
                        "type": "arm_template"
                    })
            except (json.JSONDecodeError, UnicodeDecodeError) as e:
                raise AdfValidationError(
                    f"ARM template '{arm_file.name}' has errors.\n\n"
                    f"Error: {str(e)}"
                )
        if found_jsons:
            return found_jsons
    
    # Step 3: Check for ADF folder structure (pipeline/, dataset/, linkedService/, trigger/)
    adf_folders = {"pipeline", "dataset", "linkedservice", "linkedService", "trigger", "dataflow"}
    has_adf_structure = any(
        f.is_dir() and f.name.lower() in adf_folders 
        for f in dir_path.iterdir()
    )
    
    if has_adf_structure:
        # Parse all JSON files in ADF folders
        for json_file in json_files:
            try:
                content = json_file.read_text(encoding="utf-8-sig")
                json_obj = json.loads(content)
                found_jsons.append({
                    "filename": json_file.name,
                    "path": str(json_file),
                    "content": json_obj,
                    "type": "repo_export"
                })
            except json.JSONDecodeError as e:
                raise AdfValidationError(
                    f"File '{json_file.name}' contains invalid JSON.\n\n"
                    f"Error: {str(e)}"
                )
            except UnicodeDecodeError:
                raise AdfValidationError(
                    f"File '{json_file.name}' has encoding issues. Use UTF-8."
                )
        
        if found_jsons:
            return found_jsons
    
    # Step 4: Single JSON file at root (no folder structure)
    if len(json_files) == 1:
        json_file = json_files[0]
        try:
            content = json_file.read_text(encoding="utf-8-sig")
            json_obj = json.loads(content)
            
            if not validate_adf_json(json_obj):
                raise AdfValidationError(
                    f"File '{json_file.name}' does not appear to be a valid ADF JSON.\n\n"
                    "Expected: ARM template, pipeline, dataset, or linked service definition."
                )
            
            found_jsons.append({
                "filename": json_file.name,
                "path": str(json_file),
                "content": json_obj,
                "type": "single_json"
            })
            return found_jsons
            
        except json.JSONDecodeError as e:
            raise AdfValidationError(
                f"File '{json_file.name}' contains invalid JSON.\n\n"
                f"Error: {str(e)}"
            )
        except UnicodeDecodeError:
            raise AdfValidationError(
                f"File '{json_file.name}' has encoding issues. Use UTF-8."
            )
    
    # Step 5: Multiple JSON files without explicit ADF folder names
    # Accept if files are valid ADF artifacts (common classroom export format).
    if len(json_files) > 1 and not has_adf_structure:
        parsed_files: List[Dict[str, Any]] = []
        invalid_files: List[str] = []

        for json_file in json_files:
            try:
                content = json_file.read_text(encoding="utf-8-sig")
                json_obj = json.loads(content)

                if validate_adf_json(json_obj):
                    parsed_files.append({
                        "filename": json_file.name,
                        "path": str(json_file),
                        "content": json_obj,
                        "type": "repo_export"
                    })
                else:
                    invalid_files.append(json_file.name)
            except (json.JSONDecodeError, UnicodeDecodeError):
                invalid_files.append(json_file.name)

        if parsed_files:
            return parsed_files

        json_names = [f.name for f in json_files[:5]]
        raise AdfValidationError(
            f"ZIP contains {len(json_files)} .json files, but none appear to be valid ADF artifacts.\n\n"
            f"Files: {', '.join(json_names)}\n\n"
            "Please provide valid ADF JSON files (pipeline, dataset, linkedService, trigger, dataflow, or ARM template)."
        )
    
    # Fallback: No valid ADF found
    raise AdfValidationError(
        "No valid ADF JSON files found.\n\n"
        "Expected: ARM template, single pipeline JSON, or ADF folder structure."
    )


def find_adf_text_files(directory: str) -> List[Dict[str, Any]]:
    """Find optional .txt companion files in ADF submissions.

    Args:
        directory: Path to search

    Returns:
        List of dicts with filename/path/content for UTF-8 readable text files.
    """
    dir_path = Path(directory).resolve()
    if not dir_path.exists() or not dir_path.is_dir():
        return []

    text_files: List[Dict[str, Any]] = []
    for text_file in dir_path.rglob("*.txt"):
        if not text_file.is_file():
            continue
        try:
            content = text_file.read_text(encoding="utf-8").strip()
        except UnicodeDecodeError:
            continue

        text_files.append({
            "filename": text_file.name,
            "path": str(text_file),
            "content": content,
            "type": "text_note",
        })

    return text_files
