"""
SpecularCore: The main orchestrator for spec-driven development.

This module provides the high-level API for compiling specs to code.
It delegates to specialized modules for parsing, compilation, and testing.
"""

import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

from .config import SpecSoloistConfig
from .parser import SpecParser
from .compiler import SpecCompiler
from .runner import TestRunner
from .resolver import DependencyResolver, DependencyGraph
from .manifest import BuildManifest, IncrementalBuilder, compute_content_hash
from .providers import LLMProvider
from .parser import ParsedSpec
from .schema import Arrangement


@dataclass
class BuildResult:
    """Result of a multi-spec build operation."""
    success: bool
    specs_compiled: List[str]
    specs_skipped: List[str]  # Skipped due to no changes (incremental)
    specs_failed: List[str]
    build_order: List[str]
    errors: Dict[str, str]  # spec name -> error message


class SpecSoloistCore:
    """
    Main orchestrator for the SpecSoloist framework.

    Coordinates spec parsing, code compilation, test generation,
    and the self-healing fix loop.
    """

    def __init__(
        self,
        root_dir: str = ".",
        api_key: Optional[str] = None,
        config: Optional[SpecSoloistConfig] = None
    ):
        """
        Initialize SpecSoloistCore.

        Args:
            root_dir: Project root directory.
            api_key: LLM API key (deprecated, use config instead).
            config: Full configuration object. If not provided,
                   loads from environment variables.
        """
        # Build configuration
        if config:
            self.config = config
        else:
            self.config = SpecSoloistConfig.from_env(root_dir)
            if api_key:
                self.config.api_key = api_key

        # Legacy attributes for backwards compatibility
        self.root_dir = os.path.abspath(root_dir)
        self.src_dir = self.config.src_path
        self.build_dir = self.config.build_path
        self.api_key = self.config.api_key

        # Ensure directories exist
        self.config.ensure_directories()

        # Initialize components
        self.parser = SpecParser(self.config.src_path)
        self.runner = TestRunner(self.config.build_path, config=self.config)
        self.resolver = DependencyResolver(self.parser)
        self._compiler: Optional[SpecCompiler] = None
        self._provider: Optional[LLMProvider] = None
        self._manifest: Optional[BuildManifest] = None

    def _get_manifest(self) -> BuildManifest:
        """Lazily load the build manifest."""
        if self._manifest is None:
            self._manifest = BuildManifest.load(self.config.build_path)
        return self._manifest

    def _save_manifest(self):
        """Save the build manifest to disk."""
        if self._manifest is not None:
            self._manifest.save(self.config.build_path)

    def _get_provider(self) -> LLMProvider:
        """Lazily create the LLM provider."""
        if self._provider is None:
            self._provider = self.config.create_provider()
        return self._provider

    def _get_compiler(self) -> SpecCompiler:
        """Lazily create the compiler with global context."""
        if self._compiler is None:
            global_context = self.parser.load_global_context()
            self._compiler = SpecCompiler(
                provider=self._get_provider(),
                global_context=global_context
            )
        return self._compiler

    # =========================================================================
    # Public API - Spec Management
    # =========================================================================

    def list_specs(self) -> List[str]:
        """List all available specification files."""
        return self.parser.list_specs()

    def read_spec(self, name: str) -> str:
        """Read the content of a specification file."""
        return self.parser.read_spec(name)

    def create_spec(
        self,
        name: str,
        description: str,
        type: str = "function"
    ) -> str:
        """
        Create a new specification file from the template.

        Args:
            name: Component name (e.g., "auth" creates "auth.spec.md").
            description: Brief description of the component.
            type: Component type ("function", "class", "module", "typedef").

        Returns:
            Success message with path to created file.
        """
        path = self.parser.create_spec(name, description, spec_type=type)
        return f"Created spec: {path}"

    def validate_spec(self, name: str) -> Dict[str, Any]:
        """
        Validate a spec for basic structure and SRS compliance.

        Returns:
            Dict with 'valid' (bool) and 'errors' (list) keys.
        """
        return self.parser.validate_spec(name)

    # =========================================================================
    # Public API - Verification
    # =========================================================================

    def verify_project(self) -> Dict[str, Any]:
        """
        Verifies all specs in the project for strict schema compliance
        and dependency integrity.
        
        Returns:
            Dict containing verification results per spec and global success status.
        """
        specs = self.parser.list_specs()
        results = {}
        all_passed = True
        
        # 1. Check for missing or circular dependencies
        try:
            self.resolver.resolve_build_order()  # validates no circular deps
            graph = self.resolver.build_graph()
        except Exception as e:
            return {
                "success": False,
                "error": str(e),
                "results": {}
            }

        for spec_file in specs:
            spec_name = spec_file.replace(".spec.md", "")
            try:
                # Basic validation
                basic_valid = self.validate_spec(spec_name)
                errors = basic_valid.get("errors", [])
                
                # Dependency validation
                deps = graph.get_dependencies(spec_name)
                missing_schemas = []
                
                # Schema validation
                spec = self.parser.parse_spec(spec_name)
                
                status = "valid"
                if not basic_valid["valid"]:
                    status = "invalid"
                    all_passed = False
                elif not spec.schema:
                    status = "warning"
                
                # Check if dependencies have schemas
                for dep_name in deps:
                    try:
                        dep_spec = self.parser.parse_spec(dep_name)
                        if not dep_spec.schema:
                            missing_schemas.append(dep_name)
                    except Exception:
                        pass # resolver should have caught missing files
                
                # Orchestration Step Verification
                step_errors = []
                if spec.schema and spec.schema.steps:
                    step_errors = self._verify_orchestrator_steps(spec)
                
                message = ""
                if not spec.schema:
                    message = "Missing ```yaml:schema block. "
                if missing_schemas:
                    message += f"Deps missing schemas: {', '.join(missing_schemas)}"
                if step_errors:
                    message += " | Step errors: " + "; ".join(step_errors)
                
                if step_errors:
                    status = "invalid"
                    all_passed = False

                results[spec_name] = {
                    "status": status,
                    "schema_defined": spec.schema is not None,
                    "errors": errors + step_errors,
                    "message": message.strip()
                }
                
                if status == "invalid":
                    all_passed = False
                    
            except Exception as e:
                results[spec_name] = {
                    "status": "error", 
                    "errors": [str(e)]
                }
                all_passed = False
                
        return {
            "success": all_passed,
            "results": results
        }

    def _verify_orchestrator_steps(self, spec: 'ParsedSpec') -> List[str]:
        """Verifies that all steps in an orchestrator have valid data flow."""
        if not spec.schema or not spec.schema.steps:
            return []
            
        errors = []
        # Keep track of outputs available at each step
        # Format: step_name -> {param_name -> ParameterDefinition}
        available_outputs = {
            "inputs": spec.schema.inputs
        }
        
        for step in spec.schema.steps:
            # 1. Does the target spec exist?
            try:
                target_spec = self.parser.parse_spec(step.spec)
            except Exception:
                errors.append(f"Step '{step.name}' references missing spec: {step.spec}")
                continue
                
            if not target_spec.schema:
                errors.append(f"Step '{step.name}' spec '{step.spec}' has no schema")
                continue
                
            # 2. Check inputs
            for input_name, source in step.inputs.items():
                # Source should be 'step_name.outputs.param_name'
                if "." not in source:
                    errors.append(f"Step '{step.name}' input '{input_name}' source '{source}' must be in 'step.outputs.param' format")
                    continue
                    
                parts = source.split(".")
                if len(parts) != 3 or parts[1] != "outputs":
                    # Special case for 'inputs.param_name'
                    if len(parts) == 2 and parts[0] == "inputs":
                        source_step = "inputs"
                        source_param = parts[1]
                    else:
                        errors.append(f"Step '{step.name}' input '{input_name}' source '{source}' format invalid")
                        continue
                else:
                    source_step = parts[0]
                    source_param = parts[2]
                
                if source_step not in available_outputs:
                    errors.append(f"Step '{step.name}' input '{input_name}' source step '{source_step}' not found or not yet executed")
                    continue
                    
                if source_param not in available_outputs[source_step]:
                    errors.append(f"Step '{step.name}' input '{input_name}' source param '{source_param}' not found in '{source_step}'")
                    continue
                    
                # 3. Type compatibility check
                out_def = available_outputs[source_step][source_param]
                in_def = target_spec.schema.inputs.get(input_name)
                
                if not in_def:
                    errors.append(f"Step '{step.name}' spec '{step.spec}' has no input named '{input_name}'")
                    continue
                    
                # 3. Type compatibility check
                if out_def.type != in_def.type:
                    errors.append(f"Step '{step.name}' input '{input_name}' type mismatch: {out_def.type} (source) vs {in_def.type} (target)")
                    continue

                if not out_def.compatible_with(in_def):
                    reason = ""
                    if in_def.minimum is not None and (out_def.minimum is None or out_def.minimum < in_def.minimum):
                        reason = f"source minimum ({out_def.minimum}) is less than target minimum ({in_def.minimum})"
                    elif in_def.maximum is not None and (out_def.maximum is None or out_def.maximum > in_def.maximum):
                        reason = f"source maximum ({out_def.maximum}) is greater than target maximum ({in_def.maximum})"
                    
                    errors.append(f"Step '{step.name}' input '{input_name}' constraint mismatch: {reason}")

            # Record this step's outputs for future steps
            available_outputs[step.name] = target_spec.schema.outputs
            
        return errors


    # =========================================================================
    # Public API - Compilation
    # =========================================================================

    def compile_spec(
        self,
        name: str,
        model: Optional[str] = None,
        skip_tests: bool = False,
        arrangement: Optional[Arrangement] = None
    ) -> str:
        """
        Compile a spec to implementation code.

        Args:
            name: Spec filename (with or without .spec.md extension).
            model: Override the default LLM model (optional).
            skip_tests: If True, don't generate tests (default for typedef specs).
            arrangement: Optional build arrangement.

        Returns:
            Success message with path to generated code.
        """
        # Validate first
        validation = self.validate_spec(name)
        if not validation["valid"]:
            raise ValueError(
                f"Cannot compile invalid spec: {validation['errors']}"
            )

        # Parse and compile
        spec = self.parser.parse_spec(name)
        compiler = self._get_compiler()

        # Use appropriate compilation method based on spec type
        if spec.metadata.type == "typedef":
            code = compiler.compile_typedef(spec, model=model, arrangement=arrangement)
        elif spec.metadata.type == "orchestrator":
            code = compiler.compile_orchestrator(spec, model=model, arrangement=arrangement)
        else:
            code = compiler.compile_code(spec, model=model, arrangement=arrangement)

        # Determine output path and language
        if arrangement:
            output_path = arrangement.output_paths.implementation
            # Ensure output path is relative to project root or absolute as intended
            # For now, we'll write it directly using runner.write_file if it's a specific path
            full_path = self.runner.write_file(output_path, code)
            return f"Compiled to {full_path}"
        else:
            module_name = self.parser.get_module_name(name)
            output_path = self.runner.write_code(
                module_name, code, language=spec.metadata.language_target
            )
            return f"Compiled to {output_path}"

    def compile_tests(self, name: str, model: Optional[str] = None, arrangement: Optional[Arrangement] = None) -> str:
        """
        Generate a test suite for a spec.

        Args:
            name: Spec filename (with or without .spec.md extension).
            model: Override the default LLM model (optional).
            arrangement: Optional build arrangement.

        Returns:
            Success message with path to generated tests.
        """
        spec = self.parser.parse_spec(name)

        # Skip test generation for typedef specs
        if spec.metadata.type == "typedef":
            return f"Skipped tests for typedef spec: {name}"

        compiler = self._get_compiler()
        code = compiler.compile_tests(spec, model=model, arrangement=arrangement)

        if arrangement:
            output_path = arrangement.output_paths.tests
            full_path = self.runner.write_file(output_path, code)
            return f"Generated tests at {full_path}"
        else:
            module_name = self.parser.get_module_name(name)
            output_path = self.runner.write_tests(
                module_name, code, language=spec.metadata.language_target
            )
            return f"Generated tests at {output_path}"

    def compile_project(
        self,
        specs: List[str] = None,
        model: Optional[str] = None,
        generate_tests: bool = True,
        incremental: bool = False,
        parallel: bool = False,
        max_workers: int = 4,
        arrangement: Optional[Arrangement] = None
    ) -> BuildResult:
        """
        Compile multiple specs in dependency order.

        This is the main entry point for building a project with
        interdependent specs.

        Args:
            specs: List of spec names to compile. If None, compiles all specs.
            model: Override the default LLM model (optional).
            generate_tests: Whether to generate tests for non-typedef specs.
            incremental: If True, only recompile specs that have changed.
            parallel: If True, compile independent specs concurrently.
            max_workers: Maximum number of parallel compilation workers.
            arrangement: Optional build arrangement.

        Returns:
            BuildResult with compilation status and details.
        """
        if parallel:
            return self._compile_project_parallel(
                specs, model, generate_tests, incremental, max_workers, arrangement
            )
        else:
            return self._compile_project_sequential(
                specs, model, generate_tests, incremental, arrangement
            )

    def _compile_project_sequential(
        self,
        specs: List[str],
        model: Optional[str],
        generate_tests: bool,
        incremental: bool,
        arrangement: Optional[Arrangement] = None
    ) -> BuildResult:
        """Sequential compilation - original implementation."""
        # Resolve build order
        build_order = self.resolver.resolve_build_order(specs)

        # For incremental builds, determine what needs rebuilding
        specs_to_build = set(build_order)
        if incremental:
            specs_to_build = set(self._get_incremental_build_list(build_order))

        compiled = []
        skipped = []
        failed = []
        errors = {}

        for spec_name in build_order:
            if spec_name not in specs_to_build:
                skipped.append(spec_name)
                continue

            result = self._compile_single_spec(spec_name, model, generate_tests, arrangement)
            if result["success"]:
                compiled.append(spec_name)
            else:
                failed.append(spec_name)
                errors[spec_name] = result["error"]

        # Save manifest after build
        self._save_manifest()

        return BuildResult(
            success=len(failed) == 0,
            specs_compiled=compiled,
            specs_skipped=skipped,
            specs_failed=failed,
            build_order=build_order,
            errors=errors
        )

    def _compile_project_parallel(
        self,
        specs: List[str],
        model: Optional[str],
        generate_tests: bool,
        incremental: bool,
        max_workers: int,
        arrangement: Optional[Arrangement] = None
    ) -> BuildResult:
        """Parallel compilation - compiles independent specs concurrently."""
        # Get build order grouped by levels
        levels = self.resolver.get_parallel_build_order(specs)
        build_order = [spec for level in levels for spec in level]

        # For incremental builds, determine what needs rebuilding
        specs_to_build = set(build_order)
        if incremental:
            specs_to_build = set(self._get_incremental_build_list(build_order))

        compiled = []
        skipped = []
        failed = []
        errors = {}

        # Process each level - specs within a level can be compiled in parallel
        for level in levels:
            level_to_build = [s for s in level if s in specs_to_build]
            level_skipped = [s for s in level if s not in specs_to_build]
            skipped.extend(level_skipped)

            if not level_to_build:
                continue

            # Compile this level in parallel
            with ThreadPoolExecutor(max_workers=min(max_workers, len(level_to_build))) as executor:
                futures = {
                    executor.submit(self._compile_single_spec, spec_name, model, generate_tests, arrangement): spec_name
                    for spec_name in level_to_build
                }

                for future in as_completed(futures):
                    spec_name = futures[future]
                    result = future.result()
                    if result["success"]:
                        compiled.append(spec_name)
                    else:
                        failed.append(spec_name)
                        errors[spec_name] = result["error"]

        # Save manifest after build
        self._save_manifest()

        return BuildResult(
            success=len(failed) == 0,
            specs_compiled=compiled,
            specs_skipped=skipped,
            specs_failed=failed,
            build_order=build_order,
            errors=errors
        )

    def _compile_single_spec(
        self,
        spec_name: str,
        model: Optional[str],
        generate_tests: bool,
        arrangement: Optional[Arrangement] = None
    ) -> Dict[str, Any]:
        """
        Compile a single spec and return result.

        Returns dict with 'success' (bool) and 'error' (str if failed).
        """
        try:
            # Parse spec for metadata
            spec = self.parser.parse_spec(spec_name)
            lang = arrangement.target_language if arrangement else spec.metadata.language_target
            spec_hash = compute_content_hash(spec.content)
            deps = [d.get("from", "").replace(".spec.md", "")
                    for d in spec.metadata.dependencies
                    if isinstance(d, dict)]

            # Compile the spec
            self.compile_spec(spec_name, model=model, arrangement=arrangement)

            # Determine output files
            if arrangement:
                output_files = [
                    os.path.basename(arrangement.output_paths.implementation)
                ]
                if generate_tests and spec.metadata.type != "typedef":
                    self.compile_tests(spec_name, model=model, arrangement=arrangement)
                    output_files.append(os.path.basename(arrangement.output_paths.tests))
            else:
                code_path = os.path.basename(self.runner.get_code_path(spec_name, language=lang))
                output_files = [code_path]
                
                # Generate tests if requested and not a typedef
                if generate_tests and spec.metadata.type != "typedef":
                    self.compile_tests(spec_name, model=model)
                    test_path = os.path.basename(self.runner.get_test_path(spec_name, language=lang))
                    output_files.append(test_path)

            # Update manifest
            manifest = self._get_manifest()
            manifest.update_spec(spec_name, spec_hash, deps, output_files)

            return {"success": True, "error": ""}

        except Exception as e:
            return {"success": False, "error": str(e)}

    def _get_incremental_build_list(self, build_order: List[str]) -> List[str]:
        """Determine which specs need rebuilding for incremental build."""
        manifest = self._get_manifest()
        builder = IncrementalBuilder(manifest, self.config.src_path)

        # Compute current hashes and deps
        spec_hashes = {}
        spec_deps = {}

        for spec_name in build_order:
            spec = self.parser.parse_spec(spec_name)
            spec_hashes[spec_name] = compute_content_hash(spec.content)
            spec_deps[spec_name] = [
                d.get("from", "").replace(".spec.md", "")
                for d in spec.metadata.dependencies
                if isinstance(d, dict)
            ]

        return builder.get_rebuild_plan(build_order, spec_hashes, spec_deps)

    def get_build_order(self, specs: List[str] = None) -> List[str]:
        """
        Get the build order for specs without actually compiling.

        Useful for previewing what would be built and in what order.

        Args:
            specs: List of spec names. If None, includes all specs.

        Returns:
            List of spec names in build order.
        """
        return self.resolver.resolve_build_order(specs)

    def get_dependency_graph(self, specs: List[str] = None) -> DependencyGraph:
        """
        Get the dependency graph for specs.

        Args:
            specs: List of spec names. If None, includes all specs.

        Returns:
            DependencyGraph showing relationships between specs.
        """
        return self.resolver.build_graph(specs)

    # =========================================================================
    # Public API - Testing
    # =========================================================================

    def run_tests(self, name: str) -> Dict[str, Any]:
        """
        Run the tests for a specific component.

        Returns:
            Dict with 'success' (bool) and 'output' (str) keys.
        """
        spec = self.parser.parse_spec(name)
        module_name = self.parser.get_module_name(name)
        result = self.runner.run_tests(
            module_name, language=spec.metadata.language_target
        )
        return {
            "success": result.success,
            "output": result.output
        }

    def run_all_tests(self) -> Dict[str, Any]:
        """
        Run tests for all compiled specs.

        Returns:
            Dict with overall 'success' and per-spec results.
        """
        specs = self.parser.list_specs()
        results = {}
        all_passed = True

        for spec_file in specs:
            spec_name = spec_file.replace(".spec.md", "")
            spec = self.parser.parse_spec(spec_name)
            lang = spec.metadata.language_target

            # Skip typedef specs (no tests)
            if spec.metadata.type == "typedef":
                results[spec_name] = {"success": True, "output": "Skipped (typedef)"}
                continue

            # Check if test file exists
            if not self.runner.test_exists(spec_name, language=lang):
                results[spec_name] = {"success": True, "output": "No tests found"}
                continue

            result = self.run_tests(spec_name)
            results[spec_name] = result
            if not result["success"]:
                all_passed = False

        return {
            "success": all_passed,
            "results": results
        }

    # =========================================================================
    # Public API - Self-Healing
    # =========================================================================

    def attempt_fix(self, name: str, model: Optional[str] = None, arrangement: Optional[Arrangement] = None) -> str:
        """
        Attempt to fix a failing component.

        Analyzes test output and rewrites either the code or tests
        to fix the failure.

        Args:
            name: Spec filename.
            model: Override the default LLM model (optional).
            arrangement: Optional build arrangement.

        Returns:
            Status message describing what was fixed.
        """
        module_name = self.parser.get_module_name(name)
        spec = self.parser.parse_spec(name)
        lang = arrangement.target_language if arrangement else spec.metadata.language_target

        # 1. Run tests to get current error
        if arrangement:
            # If we have an arrangement, we use its test command
            # For now, we still rely on the runner to execute it, but we might need to 
            # customize the runner or just use shell directly
            # Let's see if runner can handle custom commands
            result = self.runner.run_custom_test(arrangement.build_commands.test)
        else:
            result = self.runner.run_tests(module_name, language=lang)

        if result.success:
            return "Tests already passed. No fix needed."

        # 2. Gather context
        if arrangement:
            code_content = self.runner.read_file(arrangement.output_paths.implementation) or ""
            test_content = self.runner.read_file(arrangement.output_paths.tests) or ""
        else:
            code_content = self.runner.read_code(module_name, language=lang) or ""
            test_content = self.runner.read_tests(module_name, language=lang) or ""

        # 3. Generate fix
        compiler = self._get_compiler()
        response = compiler.generate_fix(
            spec=spec,
            code_content=code_content,
            test_content=test_content,
            error_log=result.output,
            model=model,
            arrangement=arrangement
        )

        # 4. Parse and apply fixes
        fixes = compiler.parse_fix_response(response)

        if not fixes:
            return (
                f"LLM analyzed the error but provided no formatted fix.\n"
                f"Response:\n{response}"
            )

        changes_made = []
        for filename, content in fixes.items():
            path = self.runner.write_file(filename, content)
            changes_made.append(os.path.basename(path))

        return f"Applied fixes to: {', '.join(changes_made)}. Run tests again to verify."


    