"""
Local JSON Storage Layer
"""

import json
import shutil
import uuid
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Type, TypeVar

from opentester.core.config import get_settings
from opentester.models.project import TestProject
from opentester.models.execution import TestResult, StepResult
from opentester.models.template import DSLTemplate, TemplateCreate, TemplateUpdate


T = TypeVar("T")


class StorageError(Exception):
    """Storage operation error."""
    pass


class ProjectStorage:
    """Project storage using JSON files."""

    def __init__(self):
        self.settings = get_settings()
        self._ensure_dirs()

    def _ensure_dirs(self):
        """Ensure storage directories exist."""
        self.settings.PROJECTS_DIR.mkdir(parents=True, exist_ok=True)

    def _project_path(self, project_id: str) -> Path:
        """Get path to project file."""
        return self.settings.PROJECTS_DIR / f"{project_id}.json"

    def _project_cases_path(self, project_id: str) -> Path:
        """Get path to project cases directory."""
        return self.settings.PROJECTS_DIR / project_id

    def list_projects(self) -> List[Dict]:
        """List all projects metadata."""
        projects = []
        for file_path in self.settings.PROJECTS_DIR.glob("*.json"):
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    # Return summary only
                    projects.append({
                        "id": data.get("id"),
                        "name": data.get("name"),
                        "target_type": data.get("target", {}).get("type"),
                        "created_at": data.get("created_at"),
                        "updated_at": data.get("updated_at"),
                        "case_count": len(data.get("cases", {})),
                        "group_count": len(data.get("groups", [])),
                    })
            except (json.JSONDecodeError, IOError):
                continue
        return sorted(projects, key=lambda x: x.get("created_at", ""), reverse=True)

    def get_project(self, project_id: str) -> Optional[TestProject]:
        """Get a project by ID."""
        file_path = self._project_path(project_id)
        if not file_path.exists():
            return None

        try:
            with open(file_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            return TestProject.model_validate(data)
        except (json.JSONDecodeError, IOError, KeyError):
            return None

    def save_project(self, project: TestProject) -> None:
        """Save a project to storage."""
        file_path = self._project_path(project.id)
        project.updated_at = datetime.now()

        def _serialize(obj):
            """JSON serializer for objects not serializable by default."""
            if isinstance(obj, datetime):
                return obj.isoformat()
            raise TypeError(f"Type {type(obj)} not serializable")

        try:
            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(project.model_dump(), f, ensure_ascii=False, indent=2, default=_serialize)
        except IOError as e:
            raise StorageError(f"Failed to save project: {e}")

    def delete_project(self, project_id: str) -> bool:
        """Delete a project and its cases."""
        file_path = self._project_path(project_id)
        cases_dir = self._project_cases_path(project_id)

        success = True

        # Delete project file
        if file_path.exists():
            try:
                file_path.unlink()
            except IOError:
                success = False

        # Delete cases directory
        if cases_dir.exists():
            try:
                shutil.rmtree(cases_dir)
            except IOError:
                success = False

        return success

    def project_exists(self, project_id: str) -> bool:
        """Check if a project exists."""
        return self._project_path(project_id).exists()

    def get_case(self, case_id: str) -> Optional[Dict]:
        """Get a test case by ID across all projects."""
        for project_file in self.settings.PROJECTS_DIR.glob("*.json"):
            try:
                with open(project_file, "r", encoding="utf-8") as f:
                    project_data = json.load(f)

                cases = project_data.get("cases", {})
                if case_id in cases:
                    case = cases[case_id]
                    return {
                        "id": case_id,
                        "project_id": project_data.get("id"),
                        "name": case.get("name", "Unnamed"),
                        "description": case.get("description", ""),
                        "execution_mode": case.get("execution_mode", "screen"),
                        "steps": case.get("steps", []),
                        "dsl_content": case.get("dsl_content", ""),
                        "created_at": case.get("created_at", ""),
                        "updated_at": case.get("updated_at", ""),
                    }
            except (json.JSONDecodeError, IOError):
                continue
        return None


class ExecutionStorage:
    """Execution history storage using JSON files."""

    def __init__(self):
        self.settings = get_settings()
        self._ensure_dirs()

    def _ensure_dirs(self):
        """Ensure storage directories exist."""
        self.settings.EXECUTIONS_DIR.mkdir(parents=True, exist_ok=True)

    def _execution_path(self, execution_id: str) -> Path:
        """Get path to execution file."""
        return self.settings.EXECUTIONS_DIR / f"{execution_id}.json"

    def save_execution(self, execution_data: dict) -> None:
        """Save execution to storage."""
        execution_id = execution_data.get("execution_id")
        if not execution_id:
            return

        file_path = self._execution_path(execution_id)

        def _serialize(obj):
            """JSON serializer for objects not serializable by default."""
            if isinstance(obj, datetime):
                return obj.isoformat()
            raise TypeError(f"Type {type(obj)} not serializable")

        try:
            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(execution_data, f, ensure_ascii=False, indent=2, default=_serialize)
        except IOError as e:
            raise StorageError(f"Failed to save execution: {e}")

    def get_execution(self, execution_id: str) -> Optional[dict]:
        """Get execution by ID."""
        file_path = self._execution_path(execution_id)
        if not file_path.exists():
            return None

        try:
            with open(file_path, "r", encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError):
            return None

    def list_executions(
        self,
        project_id: Optional[str] = None,
        case_id: Optional[str] = None,
        limit: int = 100,
        offset: int = 0
    ) -> List[dict]:
        """List executions with optional filtering."""
        executions = []

        for file_path in self.settings.EXECUTIONS_DIR.glob("*.json"):
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    data = json.load(f)

                # Apply filters
                if project_id and data.get("project_id") != project_id:
                    continue
                if case_id and data.get("case_id") != case_id:
                    continue

                executions.append(data)
            except (json.JSONDecodeError, IOError):
                continue

        # Sort by start time (newest first)
        executions.sort(key=lambda x: x.get("start_time", ""), reverse=True)

        return executions[offset:offset + limit]

    def delete_execution(self, execution_id: str) -> bool:
        """Delete an execution."""
        file_path = self._execution_path(execution_id)
        if file_path.exists():
            try:
                file_path.unlink()
                return True
            except IOError:
                return False
        return False

    def get_project_history(self, project_id: str, limit: int = 100) -> List[dict]:
        """Get execution history for a project."""
        return self.list_executions(project_id=project_id, limit=limit)


class Storage:
    """Unified storage interface."""

    def __init__(self):
        self.project = get_project_storage()

    def get_project(self, project_id: str) -> Optional[Dict]:
        """Get a project by ID."""
        proj = self.project.get_project(project_id)
        if proj:
            return proj.model_dump()
        return None

    def get_case(self, case_id: str) -> Optional[Dict]:
        """Get a test case by ID."""
        return self.project.get_case(case_id)


# Singleton instances
_project_storage: Optional[ProjectStorage] = None
_storage: Optional[Storage] = None
_execution_storage: Optional[ExecutionStorage] = None


def get_project_storage() -> ProjectStorage:
    """Get project storage singleton."""
    global _project_storage
    if _project_storage is None:
        _project_storage = ProjectStorage()
    return _project_storage


def get_storage() -> Storage:
    """Get storage singleton."""
    global _storage
    if _storage is None:
        _storage = Storage()
    return _storage


def get_execution_storage() -> ExecutionStorage:
    """Get execution storage singleton."""
    global _execution_storage
    if _execution_storage is None:
        _execution_storage = ExecutionStorage()
    return _execution_storage


class TemplateStorage:
    """Template storage using JSON files."""

    def __init__(self):
        self.settings = get_settings()
        self._ensure_dirs()

    def _ensure_dirs(self):
        """Ensure storage directories exist."""
        self.settings.TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)

    def _template_path(self, template_id: str) -> Path:
        """Get path to template file."""
        return self.settings.TEMPLATES_DIR / f"{template_id}.json"

    def create_template(self, data: TemplateCreate) -> DSLTemplate:
        """Create a new template."""
        template = DSLTemplate(
            id=str(uuid.uuid4()),
            name=data.name,
            description=data.description,
            target_type=data.target_type,
            dsl_template=data.dsl_template,
            variables=data.variables,
            metadata=data.metadata or DSLTemplate.__fields__['metadata'].default_factory(),
        )
        self._save_template(template)
        return template

    def _save_template(self, template: DSLTemplate) -> None:
        """Save template to storage."""
        file_path = self._template_path(template.id)
        template.updated_at = datetime.now().isoformat()
        try:
            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(template.model_dump(), f, ensure_ascii=False, indent=2)
        except IOError as e:
            raise StorageError(f"Failed to save template: {e}")

    def get_template(self, template_id: str) -> Optional[DSLTemplate]:
        """Get a template by ID."""
        file_path = self._template_path(template_id)
        if not file_path.exists():
            return None
        try:
            with open(file_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            return DSLTemplate.model_validate(data)
        except (json.JSONDecodeError, IOError):
            return None

    def list_templates(
        self,
        category: Optional[str] = None,
        target_type: Optional[str] = None,
        tag: Optional[str] = None,
    ) -> List[Dict]:
        """List all templates with optional filtering."""
        templates = []
        for file_path in self.settings.TEMPLATES_DIR.glob("*.json"):
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    data = json.load(f)

                # Apply filters
                if category and data.get("metadata", {}).get("category") != category:
                    continue
                if target_type and data.get("target_type") != target_type:
                    continue
                if tag and tag not in data.get("metadata", {}).get("tags", []):
                    continue

                templates.append({
                    "id": data.get("id"),
                    "name": data.get("name"),
                    "description": data.get("description"),
                    "target_type": data.get("target_type"),
                    "category": data.get("metadata", {}).get("category", "other"),
                    "tags": data.get("metadata", {}).get("tags", []),
                    "variable_count": len(data.get("variables", [])),
                    "usage_count": data.get("usage_count", 0),
                    "created_at": data.get("created_at"),
                    "updated_at": data.get("updated_at"),
                })
            except (json.JSONDecodeError, IOError):
                continue

        # Sort by usage count (most used first) then by name
        templates.sort(key=lambda x: (-x["usage_count"], x["name"]))
        return templates

    def update_template(self, template_id: str, data: TemplateUpdate) -> Optional[DSLTemplate]:
        """Update an existing template."""
        template = self.get_template(template_id)
        if not template:
            return None

        if data.name is not None:
            template.name = data.name
        if data.description is not None:
            template.description = data.description
        if data.dsl_template is not None:
            template.dsl_template = data.dsl_template
        if data.variables is not None:
            template.variables = data.variables
        if data.metadata is not None:
            template.metadata = data.metadata

        self._save_template(template)
        return template

    def delete_template(self, template_id: str) -> bool:
        """Delete a template."""
        file_path = self._template_path(template_id)
        if file_path.exists():
            try:
                file_path.unlink()
                return True
            except IOError:
                return False
        return False

    def increment_usage(self, template_id: str) -> bool:
        """Increment template usage count."""
        template = self.get_template(template_id)
        if template:
            template.usage_count += 1
            self._save_template(template)
            return True
        return False

    def instantiate_template(
        self,
        template_id: str,
        variables: Dict[str, any],
    ) -> Optional[str]:
        """Instantiate a template with variable values.

        Returns the instantiated DSL YAML string.
        """
        template = self.get_template(template_id)
        if not template:
            return None

        dsl = template.dsl_template

        # Replace all provided variables in template
        for var_name, var_value in variables.items():
            if var_value is not None:
                placeholder = f"${{{var_name}}}"
                dsl = dsl.replace(placeholder, str(var_value))

        # Check for any remaining required template variables
        for var in template.variables:
            if var.required:
                var_value = variables.get(var.name, var.default_value)
                if var_value is None or str(var_value).strip() == "":
                    raise ValueError(f"Required variable '{var.name}' not provided")

        # Increment usage count
        self.increment_usage(template_id)

        return dsl


# Singleton instance
_template_storage: Optional[TemplateStorage] = None


def get_template_storage() -> TemplateStorage:
    """Get template storage singleton."""
    global _template_storage
    if _template_storage is None:
        _template_storage = TemplateStorage()
    return _template_storage
