import csv
import io
import json
import logging
from typing import Any, Dict, List, Optional, Union

from jsonschema import validate, ValidationError, Draft7Validator

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.text import to_type_float, to_type_int


log = logging.getLogger(__name__)


SCHEMA_CONTROL = "description"
SCHEMA_ENTITY = "patternProperties"
SCHEMA_FIELDS = "properties"
SCHEMA_FORMAT = "format"
SCHEMA_ITEMS = "items"
SCHEMA_MAXIMUM = "maximum"
SCHEMA_MINIMUM = "minimum"
SCHEMA_PATTERN = "pattern"
SCHEMA_TYPE = "type"
SCHEMA_TYPE_CHILD = "object"
SCHEMA_TYPE_DECIMAL = "number"
SCHEMA_TYPE_INTEGER = "integer"
SCHEMA_TYPE_LIST = "array"
SCHEMA_TYPE_NULL = "null"
SCHEMA_TYPE_STRING = "string"

SCHEMA_CONTROL_DELIMITER = "delimiter:"
SCHEMA_CONTROL_FIELD = "field:"
SCHEMA_LIST_DELIMITER = ";"


class SchemaTransformer:
    """
    parse delimited flat data into hierarchy with schema validation

    usage:
      patternProperties.description holds key for subsequent dictionary of properties
      if .type = array, same-level .description holds delimiter to split a single field into list

    example schema:
    {
      "patternProperties": {
        ".*": {
          "description": "city",
          "type": "object",
          "properties": {
            "name": {"type": "string"},
            "population": {"type": "integer"}
          }
        }
      }
    }
    """

    def __init__(self, drive_manager: DriveManager):
        """initialize with schema file path"""
        self.drive = drive_manager

    def run(self, command: str, params: Optional[List[str]]) -> Any:
        if command == "extract_field_names":
            if params:
                if isinstance(params[0], dict):
                    schema = params[0]
                elif isinstance(params[0], str):
                    schema_path = params[0]
                    schema = load_json(self.drive, schema_path) if schema_path else None
                else:
                    log.warning(f"{command} requires path or dictionary")
                    return None
                return self.extract_field_names(schema)
        elif command == "process_file":
            if params:
                if len(params) < 2:
                    log.warning(f"{command} requires data file path and schema path")
                    return None
                if isinstance(params[0], str) and isinstance(params[1], str):
                    file_path = params[0]
                    schema_path = params[1]
                    self.process_file(file_path, schema_path)
                    log.info(f"{command} successful")
        elif command == "validate_structure":
            if params:
                if isinstance(params[0], dict):
                    schema = params[0]
                elif isinstance(params[0], str):
                    schema_path = params[0]
                    schema = load_json(self.drive, schema_path) if schema_path else None
                else:
                    log.warning(f"{command} requires path or dictionary")
                    return False
                return self.validate_structure(schema)
            return False
        log.warning(f"{command} failed; verify parameters")
        return None

    def process_file(self, data_path: str, schema_path: str) -> bool:
        """complete processing pipeline from file paths to file path"""
        schema = load_json(self.drive, schema_path) if schema_path else None
        if not schema:
            log.error(f"failed to load schema: {schema_path}")
            return False

        content = self.drive.read_or_none(data_path)
        file_ext = extract_extension_from_filename(data_path)
        delimiter = get_delimiter_from_file_extension(file_ext)
        return self.drive.write_or_false(
            data_path.replace(file_ext, "json"),
            json.dumps(self.process_data(content, schema, delimiter)),
        )

    def process_data(
        self, data_string: str, schema: Dict[str, Any], delimiter: str = "|"
    ) -> Optional[Dict[str, Dict[str, Any]]]:
        """process: parse + validate"""
        data = self.parse_delimited(data_string, schema, delimiter)

        if not data:
            logging.warning("no valid records found")
            return None

        if not self.validate_structure(data, schema):
            logging.error("schema validation failed")
            return None

        return data

    def parse_delimited(
        self, data_string: str, schema: Dict[str, Any], delimiter: str = "|"
    ) -> Dict[str, Any]:
        """parse delimited string into into hierarchy"""
        # convert string to file-like object with CSV reader
        buffer = io.StringIO(data_string)
        reader = csv.DictReader(buffer, delimiter=delimiter)

        data = {}
        for row_num, row in enumerate(reader, start=1):
            try:
                expanded_row = self._expand_row(row, schema)
                ((entity_key, row_data),) = expanded_row.items()
                if not entity_key in data:
                    data[entity_key] = row_data
                else:
                    self._merge_children_of(data[entity_key], row_data)

            except Exception as e:
                logging.error(f"row {row_num}: {e}")
                continue

        return data

    def validate_structure(
        self, data: Dict[str, Dict[str, Any]], schema: Dict[str, Any]
    ) -> bool:
        """validate complete data structure against JSON schema"""
        try:
            validate(instance=data, schema=schema)
            log.info(f"schema validation successful for {len(data)} records")
            return True
        except ValidationError as e:
            log.error(f"schema validation failed: {e.message}")
            log.error(f"failed at path: {' -> '.join(str(p) for p in e.absolute_path)}")
            return False

    def extract_field_names(
        self, schema: Dict[str, Any], prefix: Optional[Union[bool, str]] = False
    ) -> List[str]:
        """convenience: map schema attributes for flat file header"""
        if isinstance(prefix, bool):
            prefix = "" if prefix else None

        headers: List[str] = []

        if SCHEMA_ENTITY in schema:
            ((_, entity),) = schema[SCHEMA_ENTITY].items()
            entity_key = self._extract_control(
                entity[SCHEMA_CONTROL], SCHEMA_CONTROL_FIELD
            )
            full_key = self._make_full_name(entity_key, prefix)
            headers.append(full_key)
        else:
            entity = schema

        fields = entity[SCHEMA_FIELDS]
        for field_name, field in fields.items():
            full_name = self._make_full_name(field_name, prefix)
            if field[SCHEMA_TYPE] == SCHEMA_TYPE_CHILD:
                children = self.extract_field_names(
                    field, full_name if prefix is not None else None
                )
                headers.extend(children)
            else:
                headers.append(full_name)

        return headers

    def _make_full_name(self, field_name: str, prefix: Optional[str] = None) -> str:
        return f"{prefix}.{field_name}" if prefix is not None and prefix else field_name

    def _expand_row(
        self, row: Dict[str | None, str | None], schema: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """parse delimited string into into hierarchy"""
        if SCHEMA_ENTITY in schema:
            ((_, entity),) = schema[SCHEMA_ENTITY].items()
            entity_key = self._extract_control(
                entity[SCHEMA_CONTROL], SCHEMA_CONTROL_FIELD
            )
        else:
            entity_key = None
            entity = schema

        values = {}
        fields = entity[SCHEMA_FIELDS]
        for field_name, field in fields.items():
            if field[SCHEMA_TYPE] == SCHEMA_TYPE_CHILD:
                children = self._expand_row(row, field)
                values[field_name] = children
            else:
                values[field_name] = self._to_type(row[field_name], field)

        if entity_key:
            return {row[entity_key]: values}
        return values

    def _merge_children_of(
        self, record: Dict[str, Any], update: Dict[str, Any]
    ) -> None:
        """adds dictionary entries; does not overwrite non-dictionary attributes"""
        for key, update_value in update.items():
            if isinstance(update_value, dict):
                if key in record and isinstance(record[key], dict):
                    self._merge_children_of(record[key], update_value)
                else:
                    record[key] = update_value

    def _extract_control(self, controls: str, control_prefix: str) -> str:
        return next(
            (
                control.removeprefix(control_prefix).strip()
                for control in controls.split(SCHEMA_LIST_DELIMITER)
                if control.startswith(control_prefix)
            ),
            "",
        )

    def _to_type(self, value: str, field: Dict[str, Any]) -> Any:
        """convert string to schema field type; NOTE: no validation here tyvm"""
        field_type = field[SCHEMA_TYPE]

        nullable = SCHEMA_TYPE_NULL in field_type
        if not value and nullable:
            return None

        if SCHEMA_TYPE_STRING in field_type:
            return value

        if SCHEMA_TYPE_INTEGER in field_type:
            return to_type_int(value)

        if SCHEMA_TYPE_DECIMAL in field_type:
            return to_type_float(value)

        if SCHEMA_TYPE_LIST in field_type:
            if not value:
                return []

            delimiter = self._extract_control(
                field[SCHEMA_CONTROL], SCHEMA_CONTROL_DELIMITER
            )
            if not delimiter:
                return []

            items = []
            for item in value.split(delimiter):
                items.append(self._to_type(item, field[SCHEMA_ITEMS]))
            return items

        return value
