from datetime import datetime
import json
import logging
from typing import Any, Dict, List, Optional

from arkeo.path import PathManager
from utils.access import DriveManager
from utils.file import (
    extract_extension_from_filename,
    get_delimiter_from_file_extension,
)
from utils.json import load_json
from utils.schema import SchemaTransformer
from utils.url import extract_domain


log = logging.getLogger(__name__)


DATA_NAME = "providers"
DELTA = "del"

FILE_EXT = "json"

DIY_TYPES = ["archive", "government", "social media"]  # may require extra handling


class ProviderManager:
    """manages publisher data storage and retrieval."""

    def __init__(self, path_manager: Optional[PathManager] = None):
        self.path = path_manager or PathManager()
        self.drive: DriveManager = self.path.drive
        self.data = {}
        self._data_path = ""

        self._load_data()

    @property
    def dir_data(self) -> str:
        return self.path.reference + "providers/"

    def _load_data(self) -> None:
        """load publishers from JSON file."""
        self._data_path = f"{self.dir_data}{DATA_NAME}.{FILE_EXT}"
        self.data = load_json(self.path.drive, self._data_path)

    def get_category(self, domain: str) -> str:
        """get publisher category for domain."""
        if domain not in self.data:
            self.add_publisher(domain)
        return self.data[domain].get("category", "")

    def get_name(self, domain: str) -> str:
        """get publisher name for domain."""
        if domain not in self.data:
            self.add_publisher(domain)
        return self.data[domain].get("name", "")

    def add_publisher(self, domain: str) -> bool:
        """add new publisher domain to registry."""
        domain = extract_domain(domain)
        self.data[domain] = {}
        return self.path.drive.write_or_false(self._data_path, json.dumps(self.data))

    def add_publisher_from_html(self, domain: str, html: str) -> bool:
        """add new publisher domain to registry using html metadata."""
        domain = extract_domain(domain)
        fields = {}

        schema_path = f"{self.dir_data}{DATA_NAME}-schema.{FILE_EXT}"
        schema: Dict[str, Any] = load_json(self.drive, schema_path)
        field_names = SchemaTransformer(self.drive).extract_field_names(schema)

        for field_name in field_names:
            # TODO use metdataextractor
            pass

        self.data[domain] = fields
        return self.path.drive.write_or_false(self._data_path, json.dumps(self.data))

    def apply_delta(self, delta_file_path: Optional[str] = None) -> bool:
        """
        data_name.yymmdd.bak      # full backup
        data_name.del             # partial update (unprocessed)
        data_name.yymmddhhmm.del  # partial update (processed)
        """
        today = datetime.now().strftime("%y%m%d")
        timestamp = datetime.now().strftime("%y%m%d%H%M")

        # build paths
        data_path = f"{self.dir_data}{DATA_NAME}.{FILE_EXT}"

        # handle default delta file path
        if not delta_file_path:
            delta_file_path = f"{self.dir_data}{DATA_NAME}.{DELTA}"

        # validate delta file
        if not delta_file_path.endswith(f".{DELTA}"):
            log.warning(f"malformed filename: {delta_file_path}")
            return False

        if not self.path.drive.file_exists(delta_file_path):
            log.warning(f"file does not exist: {delta_file_path}")
            return False

        # load and validate delta data
        try:
            delta_data_str = self.path.drive.read(delta_file_path)
            delta_data = json.loads(delta_data_str)
        except Exception as e:
            log.warning(f"failed to import delta: {delta_file_path} - {e}")
            return False

        # apply updates
        count = 0
        for key, value in delta_data.items():
            if key not in self.data:
                self.data[key] = value
                count += 1

        if count == 0:
            log.warning(f"no updates from file {delta_file_path}")
            return False

        # file operations with better error handling
        try:
            # create backup if it doesn't exist for today
            backup_file = f"{self.dir_data}{DATA_NAME}.{today}.bak"
            if not self.path.drive.file_exists(backup_file):
                if self.path.drive.file_exists(data_path):
                    self.path.drive.rename(data_path, backup_file)
                else:
                    log.warning(f"main data file not found: {data_path}")
                    return False

            # write updated data
            self.path.drive.write(
                data_path, json.dumps(dict(sorted(self.data.items())))
            )

            # archive the processed delta file
            arc_path = delta_file_path.replace(f".{DELTA}", f".{timestamp}.{DELTA}")
            self.path.drive.rename(delta_file_path, arc_path)

            log.info(f"processed {len(delta_data)} entries, added {count} new ones")

        except Exception as e:
            log.error(f"failed to update directory: {e}")
            return False

        return True

    def _overwrite_from_dsv(
        self, file_path: Optional[str] = "", delimiter: Optional[str] = "|"
    ) -> bool:
        """overwrite existing data from DSV file."""
        if not file_path:
            file_path = f"{self.dir_data}{DATA_NAME}.psv"
            delimiter = "|"

        content = self.path.drive.read_or_none(file_path)
        if not content:
            log.warning("verify parameter file_path.")
            return False

        records = content.split("\n")
        if not records:
            log.info(f"no records in file {file_path}.")
            return False

        if not delimiter:
            ext = extract_extension_from_filename(file_path)
            delimiter = get_delimiter_from_file_extension(ext)

        if not delimiter:
            log.warning(
                "require delimiter or file_path with extension of known delimiter"
            )
            return False

        data = {}
        for line_num, record in enumerate(records[1:]):  # skip header
            if record:
                fields = record.split(delimiter)
                domain = fields[2]
                if domain in data:
                    log.warning(f"[{line_num}] domain ({domain}) exists already")
                data[domain] = {
                    "code": fields[0],
                    "name": fields[1],
                    "alias": fields[3],
                    "category": fields[4],
                    "description": fields[5],
                }

        self.data = data
        json_path = self.dir_data + DATA_NAME + "." + FILE_EXT
        return self.path.drive.write_or_false(json_path, json.dumps(data))
