import json
import logging
import os
from typing import Any, Dict, List, Optional, Union

from tqdm import tqdm

from arkeo.keys import format_nav_path, is_nav_key_format
from arkeo.path import PathManager
from arkeo.clean import convert_markdown_to_text
from ref.geonames import GeonamesManager
from utils.access import DriveManager
from utils.alfnu import expand_range
from utils.ctx import ContextManager
from utils.json import load_json, write_jsonl_or_false, write_piped_or_false
from utils.kwd import KeywordsManager
from utils.schema import SchemaTransformer


log = logging.getLogger(__name__)


FILE_EXT = "json"
EXT_TDS = "tds"

TDS_MAP = {
    "string": "string",
    "integer": "integer",
    "number": "real",
    "boolean": "boolean",
    "date": "date",
    "datetime": "datetime",
}


class Extractor:

    def __init__(self, path_manager: PathManager):
        self.path = path_manager
        self.drive: DriveManager = self.path.drive
        self.cxm = ContextManager()
        self.kwm = KeywordsManager(GeonamesManager(self.path))

        # TODO: add context cfgs into single dictionary, self._cx
        self._cx_labor = load_json(self.drive, f"{self.path.config}context_labor.json")
        self._cx_money = load_json(self.drive, f"{self.path.config}context_money.json")

    def process(self, source: Optional[str] = None) -> bool:
        """one nav_key (YYYY-mm-pk) or multiple pks or none"""

        if source and isinstance(source, str) and is_nav_key_format(source):
            # tag one, mostly for research
            data = self.index_item(source, True)
            file_name = f"{source}.json"
        else:
            if source:
                source = source.split(",")
            data = self.index_items(source)
            file_name = "view.json"

        if not data:
            log.warning(f"{file_name}, no data to write")
            return False
        else:
            file_path = self.path.views + file_name
            if not self.drive.write_or_false(file_path, json.dumps(data)):
                log.warning(f"{file_path}, write failure")
                return False
            return True

    def index_items(
        self, keys: Union[str, List[str], Dict[str, Dict[str, str]]] = None
    ) -> List[Dict]:
        results = []

        if not (keys and isinstance(keys, dict)):
            pks = load_json(self.drive, self.pk_path)
            if keys:
                # build dict from selected keys only
                keys = {k: pks[k] for k in expand_range(keys) if k in pks}
            else:
                keys = pks

        for data in tqdm(keys.values(), desc="indexing articles"):
            item = self.index_item(data["nav_key"])
            if item:
                item["url"] = data["url"]
                results.append(item)

        return results

    def index_item(self, nav_key: str, beta: bool = False) -> Dict[str, Any]:
        file_path = f"{self.path.corpus}{format_nav_path(nav_key)}"
        for ext in [".md", ".txt"]:
            if self.drive.file_exists(file_path + ext):
                body_text = self.path.drive.read(file_path + ext)
                if ext == ".md":
                    body_text = convert_markdown_to_text(body_text)
                break
        else:
            return {}

        labor_matches = self.cxm.extract_content(body_text, self._cx_labor)
        money_matches = self.cxm.extract_content(body_text, self._cx_money)

        if not (labor_matches or money_matches):
            return {}

        if log.level == logging.DEBUG:
            self._write_snapshot(
                nav_key, {"labor": labor_matches, "money": money_matches}
            )

        return {
            "nav_key": nav_key,
            "url": file_path,
            "data": (
                {"labor": labor_matches}
                | {"money": money_matches}
                | {"geoloc": self.kwm.find_all(body_text)}
            ),
        }

    def generate_tds_from_schema(self, schema_path: str) -> bool:
        """
        - map schema types to Tableau field types
        - create XML structure with field metadata
        - include calculated field templates
        - define hierarchies from schema relationships

        args:
            schema: JSON schema dict
            data_source_path: Path to data file
        returns:
            bool: if TDS XML content written
        """

        schema = load_json(self.drive.read_or_none(schema_path))
        if not schema:
            log.warninig(f"no file: {schema_path}")
            return False

        table_name = os.path.splitext(os.path.basename(schema_path))[0]
        dir_path = os.path.dirname(os.path.abspath(schema_path))
        filename = os.path.basename(schema_path)

        lines = []
        lines.append('<?xml version="1.0" encoding="UTF-8"?>')
        lines.append(
            f'<datasource caption="{table_name}" inline="true" version="18.1">'
        )
        lines.append(
            f'  <connection class="json" directory="{dir_path}" filename="{filename}">'
        )
        lines.append(
            f'    <relation name="{table_name}" table="[{table_name}]" type="table"/>'
        )
        lines.append(f"    <metadata-records>")

        flat_properties = SchemaTransformer.flatten_schema(schema, True)
        for field_name, field_info in flat_properties.items():
            field_type = field_info.get("type", "string")
            tableau_type = TDS_MAP.get(field_type, "string")
            role = "measure" if field_type in ["integer", "number"] else "dimension"
            lines.append(
                f'      <column caption="{field_name}" datatype="{tableau_type}" name="[{field_name}]" role="{role}"/>'
            )

        lines.append("    </metadata-records>")
        lines.append("  </connection>")
        lines.append("</datasource>")

        return self.drive.write_or_false(
            schema_path.replace(FILE_EXT, EXT_TDS), "\n".join(lines)
        )

    def _write_snapshot(self, key: str, items: Dict[str, Any]) -> None:
        for filename, content in items.items():
            file_path = f"{self.path.temp}{key}-{filename}"
            write_jsonl_or_false(self.drive, file_path + ".jsonl", content)
            write_piped_or_false(self.drive, file_path + ".psv", content)
