# Path: fimfarchive/stories.py
# class Story:
#     """
#     Represents a story.
#     """
# 
#     def __init__(
#             self,
#             key: int,
#             fetcher: Fetcher = None,
#             meta: Dict[str, Any] = None,
#             data: bytes = None,
#             flavors: Iterable[Flavor] = (),
#             ) -> None:
#         """
#         Constructor.
# 
#         Args:
#             key: Primary key of the story.
#             fetcher: Fetcher to use for lazy fetching.
#             meta: Meta to populate the story with.
#             data: Data to populate the story with.
#             flavors: Content type hints.
#         """
#         if fetcher is None and (data is None or meta is None):
#             raise ValueError("Story must contain fetcher if lazy.")
# 
#         self.key = key
#         self.fetcher = fetcher
#         self.flavors = set(flavors)
#         self._meta = meta
#         self._data = data
# 
#     @property
#     def is_fetched(self) -> bool:
#         """
#         True if no more fetches are necessary.
#         """
#         return self.has_meta and self.has_data
# 
#     @property
#     def has_meta(self) -> bool:
#         """
#         True if story meta has been fetched.
#         """
#         return self._meta is not None
# 
#     @property
#     def meta(self) -> Dict[str, Any]:
#         """
#         Returns the story meta.
# 
#         Meta may be fetched if this story instance is lazy.
# 
#         Raises:
#             InvalidStoryError: If a valid story is not found.
#             StorySourceError: If source does not return any data.
#         """
#         if self._meta is None and self.fetcher:
#             self._meta = self.fetcher.fetch_meta(self.key)
# 
#         if self._meta is None:
#             raise StorySourceError("Meta is missing.")
# 
#         return self._meta
# 
#     @property
#     def has_data(self) -> bool:
#         """
#         True if story data has been fetched.
#         """
#         return self._data is not None
# 
#     @property
#     def data(self) -> bytes:
#         """
#         Returns the story data.
# 
#         Data may be fetched if this story instance is lazy.
# 
#         Raises:
#             InvalidStoryError: If a valid story is not found.
#             StorySourceError: If source does not return any data.
#         """
#         if self._data is None and self.fetcher:
#             self._data = self.fetcher.fetch_data(self.key)
# 
#         if self._data is None:
#             raise StorySourceError("Data is missing.")
# 
#         return self._data
# 
#     def merge(self, **params) -> 'Story':
#         """
#         Returns a shallow copy, optionally replacing attributes.
# 
#         Args:
#             **params: Overrides parameters from the current instance.
# 
#         Raises:
#             TypeError: If passed an unexpected parameter.
#         """
#         kwargs = {k.lstrip('_'): v for k, v in vars(self).items()}
#         kwargs.update(params)
# 
#         return type(self)(**kwargs)
# 
# Path: fimfarchive/utils.py
# class JayWalker:
#     """
#     Walker for JSON objects.
#     """
# 
#     def walk(self, data) -> None:
#         """
#         Walks the attributes of a JSON object.
# 
#         Args:
#             data: The object to walk.
#         """
#         iterator: Iterator
# 
#         if isinstance(data, dict):
#             iterator = iter(data.items())
#         elif isinstance(data, list):
#             iterator = enumerate(data)
#         else:
#             return
# 
#         for key, value in iterator:
#             self.handle(data, key, value)
# 
#     def handle(self, data, key, value) -> None:
#         """
#         Handles a single JSON entry.
# 
#         Args:
#             data: The current object.
#             key: The key of the entry.
#             value: The value of the entry.
#         """
#         self.walk(value)
# 
# Path: fimfarchive/converters/base.py
# class Converter(ABC):
#     """
#     Converter for stories.
#     """
# 
#     @abstractmethod
#     def __call__(self, story: Story) -> Story:
#         """
#         Applies the conversion.
# 
#         Args:
#             *story: The story to convert.
# 
#         Returns:
#             A converted story.
#         """


from copy import deepcopy
from typing import Any, Optional
from fimfarchive.stories import Story
from fimfarchive.utils import JayWalker
from .base import Converter
import arrow







class DateNormalizer(JayWalker):
    """
    Normalizes timezones of date values to UTC.
    """

    def handle(self, data, key, value) -> None:
        if str(key).startswith('date_'):
            data[key] = self.normalize(value)
        else:
            self.walk(value)

    def normalize(self, value: Any) -> Optional[str]:
        """
        Normalizes a single date value.
        """
        parsed = arrow.get(value or 0)

        if parsed.int_timestamp == 0:
            return None

        return parsed.to('utc').isoformat()



# Path: fimfarchive/stories.py
# class Story:
#     """
#     Represents a story.
#     """
# 
#     def __init__(
#             self,
#             key: int,
#             fetcher: Fetcher = None,
#             meta: Dict[str, Any] = None,
#             data: bytes = None,
#             flavors: Iterable[Flavor] = (),
#             ) -> None:
#         """
#         Constructor.
# 
#         Args:
#             key: Primary key of the story.
#             fetcher: Fetcher to use for lazy fetching.
#             meta: Meta to populate the story with.
#             data: Data to populate the story with.
#             flavors: Content type hints.
#         """
#         if fetcher is None and (data is None or meta is None):
#             raise ValueError("Story must contain fetcher if lazy.")
# 
#         self.key = key
#         self.fetcher = fetcher
#         self.flavors = set(flavors)
#         self._meta = meta
#         self._data = data
# 
#     @property
#     def is_fetched(self) -> bool:
#         """
#         True if no more fetches are necessary.
#         """
#         return self.has_meta and self.has_data
# 
#     @property
#     def has_meta(self) -> bool:
#         """
#         True if story meta has been fetched.
#         """
#         return self._meta is not None
# 
#     @property
#     def meta(self) -> Dict[str, Any]:
#         """
#         Returns the story meta.
# 
#         Meta may be fetched if this story instance is lazy.
# 
#         Raises:
#             InvalidStoryError: If a valid story is not found.
#             StorySourceError: If source does not return any data.
#         """
#         if self._meta is None and self.fetcher:
#             self._meta = self.fetcher.fetch_meta(self.key)
# 
#         if self._meta is None:
#             raise StorySourceError("Meta is missing.")
# 
#         return self._meta
# 
#     @property
#     def has_data(self) -> bool:
#         """
#         True if story data has been fetched.
#         """
#         return self._data is not None
# 
#     @property
#     def data(self) -> bytes:
#         """
#         Returns the story data.
# 
#         Data may be fetched if this story instance is lazy.
# 
#         Raises:
#             InvalidStoryError: If a valid story is not found.
#             StorySourceError: If source does not return any data.
#         """
#         if self._data is None and self.fetcher:
#             self._data = self.fetcher.fetch_data(self.key)
# 
#         if self._data is None:
#             raise StorySourceError("Data is missing.")
# 
#         return self._data
# 
#     def merge(self, **params) -> 'Story':
#         """
#         Returns a shallow copy, optionally replacing attributes.
# 
#         Args:
#             **params: Overrides parameters from the current instance.
# 
#         Raises:
#             TypeError: If passed an unexpected parameter.
#         """
#         kwargs = {k.lstrip('_'): v for k, v in vars(self).items()}
#         kwargs.update(params)
# 
#         return type(self)(**kwargs)
# 
# Path: fimfarchive/utils.py
# class JayWalker:
#     """
#     Walker for JSON objects.
#     """
# 
#     def walk(self, data) -> None:
#         """
#         Walks the attributes of a JSON object.
# 
#         Args:
#             data: The object to walk.
#         """
#         iterator: Iterator
# 
#         if isinstance(data, dict):
#             iterator = iter(data.items())
#         elif isinstance(data, list):
#             iterator = enumerate(data)
#         else:
#             return
# 
#         for key, value in iterator:
#             self.handle(data, key, value)
# 
#     def handle(self, data, key, value) -> None:
#         """
#         Handles a single JSON entry.
# 
#         Args:
#             data: The current object.
#             key: The key of the entry.
#             value: The value of the entry.
#         """
#         self.walk(value)
# 
# Path: fimfarchive/converters/base.py
# class Converter(ABC):
#     """
#     Converter for stories.
#     """
# 
#     @abstractmethod
#     def __call__(self, story: Story) -> Story:
#         """
#         Applies the conversion.
# 
#         Args:
#             *story: The story to convert.
# 
#         Returns:
#             A converted story.
#         """


# Path: fimfarchive/converters/local_utc.py
from copy import deepcopy
from typing import Any, Optional
from fimfarchive.stories import Story
from fimfarchive.utils import JayWalker
from .base import Converter
import arrow








class DateNormalizer(JayWalker):
    """
    Normalizes timezones of date values to UTC.
    """

    def handle(self, data, key, value) -> None:
        if str(key).startswith('date_'):
            data[key] = self.normalize(value)
        else:
            self.walk(value)

    def normalize(self, value: Any) -> Optional[str]:
        """
        Normalizes a single date value.
        """
        parsed = arrow.get(value or 0)

        if parsed.int_timestamp == 0:
            return None

        return parsed.to('utc').isoformat()



class LocalUtcConverter(Converter):