{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Main\n",
    "\n",
    "> Módulo principal com a classe Fiscaliza e Issue"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp main"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "from nbdev.showdoc import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "import copy\n",
    "import json\n",
    "import os\n",
    "import re\n",
    "from datetime import datetime, timedelta\n",
    "from functools import cached_property\n",
    "from pathlib import Path\n",
    "\n",
    "import urllib3\n",
    "from dotenv import load_dotenv\n",
    "from fastcore.xtras import listify\n",
    "from redminelib import Redmine\n",
    "from requests.exceptions import ConnectionError, SSLError\n",
    "from unidecode import unidecode\n",
    "\n",
    "from fiscaliza.attrs import FIELDS, SPECIAL_FIELDS\n",
    "from fiscaliza.constants import FLOW, STATES, STATUS, URL_HM, URL_PD\n",
    "from fiscaliza.datatypes import AtomicField"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "load_dotenv(override=True)\n",
    "\n",
    "UTFCHARS = re.compile(r\"[!\\\"#$%&'\\(\\)*+\\,\\-\\.\\/:;<=>\\?@\\[\\\\\\]\\^`_\\{\\|\\}~]\")\n",
    "\n",
    "CONDITIONAL_FIELDS = {\n",
    "    k: v.reset(v) for k, v in FIELDS.items() if getattr(v, \"mapping\", False)\n",
    "}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class Fiscaliza:\n",
    "    def __init__(\n",
    "        self, username: str, password: str, teste: bool = True, key: str = None\n",
    "    ):\n",
    "        self.username = username if key is None else os.environ[\"USERAPI\"]\n",
    "        self.password = password\n",
    "        self.teste = teste\n",
    "        self.key = key\n",
    "        self.url = URL_HM if teste else URL_PD\n",
    "        self.client = self.authenticate()\n",
    "\n",
    "    def authenticate(self):\n",
    "        try:\n",
    "            fiscaliza = Redmine(\n",
    "                self.url,\n",
    "                username=self.username,\n",
    "                password=self.password,\n",
    "                key=self.key,\n",
    "                requests={\"verify\": True},\n",
    "            )\n",
    "            fiscaliza.auth()\n",
    "        except SSLError:\n",
    "            urllib3.disable_warnings()\n",
    "            fiscaliza = Redmine(\n",
    "                self.url,\n",
    "                username=self.username,\n",
    "                password=self.password,\n",
    "                key=self.key,\n",
    "                requests={\"verify\": False},\n",
    "            )\n",
    "            fiscaliza.auth()\n",
    "\n",
    "        except ConnectionError as e:\n",
    "            raise ConnectionError(\n",
    "                \"Não foi possível conectar ao servidor do Fiscaliza\"\n",
    "            ) from e\n",
    "        return fiscaliza\n",
    "\n",
    "    def get_issue(self, issue_id: str | int):\n",
    "        return Issue(self.client, issue_id)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "class Issue:\n",
    "    def __init__(self, client: Redmine, issue_id: int | str):\n",
    "        self.client = client\n",
    "        self.id = issue_id\n",
    "        self._issue = self.client.issue.get(\n",
    "            issue_id,\n",
    "            include=[\n",
    "                \"children\",\n",
    "                \"relations\",\n",
    "                \"attachments\",\n",
    "                \"journals\",\n",
    "            ],\n",
    "        )\n",
    "        self._ascii2utf = {}\n",
    "\n",
    "    @property\n",
    "    def current_user(self):\n",
    "        user = dict(list(self.client.user.get(\"current\")))\n",
    "        return f'{user.get(\"firstname\", \"\")} {user.get(\"lastname\", \"\")}'\n",
    "\n",
    "    @staticmethod\n",
    "    def __format_json_string(field: str) -> str:\n",
    "        \"\"\"Recebe uma string formatada como json e retorna a mesma string formatada como json\"\"\"\n",
    "        string = field.replace(\"'\", '\"').replace(\"=>\", \": \")\n",
    "\n",
    "        def force_strings(obj):\n",
    "            if isinstance(obj, dict):\n",
    "                return {k: str(v) for k, v in obj.items()}\n",
    "            return obj\n",
    "\n",
    "        try:\n",
    "            return json.loads(string, object_hook=force_strings)\n",
    "        except (json.JSONDecodeError, TypeError):\n",
    "            return string\n",
    "\n",
    "    @staticmethod\n",
    "    def extract_value(field: str | int | dict | list) -> str | int | list:\n",
    "        \"\"\"Recebe uma string formatada como json e extrai os valores das chaves de acordo com o tipo de campo\"\"\"\n",
    "        if isinstance(field, str):\n",
    "            json_obj = Issue.__format_json_string(field)\n",
    "            if isinstance(json_obj, (int, float)):\n",
    "                return str(json_obj)\n",
    "            elif isinstance(json_obj, str):\n",
    "                return json_obj\n",
    "            return Issue.extract_value(json_obj)\n",
    "\n",
    "        elif isinstance(field, dict):\n",
    "            if not (valor := field.get(\"valor\")):\n",
    "                valor = field.get(\"name\", field)\n",
    "            return str(valor)\n",
    "\n",
    "        elif isinstance(field, list):\n",
    "            return [Issue.extract_value(f) for f in field]\n",
    "\n",
    "        elif isinstance(field, (int, float)) or field is None:\n",
    "            return field\n",
    "        else:\n",
    "            raise TypeError(\n",
    "                f\"O tipo de campo {type(field)} não é suportado. \"\n",
    "                f\"Por favor, reporte o erro para o desenvolvedor.\"\n",
    "            )\n",
    "\n",
    "    @property\n",
    "    def type(self) -> str:\n",
    "        if tracker := self._attrs.get(\"tracker\"):\n",
    "            return self._utf2ascii(tracker.get(\"name\", \"\"))\n",
    "\n",
    "    def relations(self) -> dict:\n",
    "        relations = {}\n",
    "        for relation in self._attrs.get(\"relations\", []):\n",
    "            issue_id = relation.get(\"issue_id\")\n",
    "            if self._attrs[\"id\"] == issue_id:\n",
    "                issue_id = relation.get(\"issue_to_id\")\n",
    "            relations[issue_id] = Issue(self.client, issue_id)\n",
    "        return relations\n",
    "\n",
    "    def update_on(self) -> str:\n",
    "        if journal := self._attrs[\"journals\"]:\n",
    "            journal = journal[-1]\n",
    "            key = \"user\"\n",
    "        else:\n",
    "            journal = self._attrs\n",
    "            key = \"author\"\n",
    "\n",
    "        user = journal[key][\"name\"]\n",
    "        date = datetime.strptime(\n",
    "            journal[\"created_on\"], \"%Y-%m-%dT%H:%M:%SZ\"\n",
    "        ) - timedelta(hours=3)\n",
    "        return f\"Atualizada por {user} em {datetime.strftime(date, '%d/%m/%Y')} às {date.time()}\"\n",
    "\n",
    "    @cached_property\n",
    "    def project_members(self) -> list:\n",
    "        project_id = Issue.extract_value(self._attrs[\"project\"]).lower()\n",
    "        return [\n",
    "            dict(member)\n",
    "            for member in self.client.project_membership.filter(\n",
    "                project_id=project_id, limit=None\n",
    "            )\n",
    "        ]\n",
    "\n",
    "    def names2id(self) -> dict:\n",
    "        return {\n",
    "            member[\"user\"][\"name\"]: member[\"user\"][\"id\"]\n",
    "            for member in self.project_members\n",
    "            if \"user\" in member\n",
    "        }\n",
    "\n",
    "    def ids2names(self) -> dict:\n",
    "        return {v: k for k, v in self.names2id().items()}\n",
    "\n",
    "    def _utf2ascii(self, s: str) -> str:\n",
    "        \"\"\"Receives a string and returns the same in ASCII format without spaces\"\"\"\n",
    "        decoded_string = unidecode(re.sub(UTFCHARS, \"\", s).replace(\" \", \"_\")).lower()\n",
    "        self._ascii2utf[decoded_string] = s\n",
    "        return decoded_string\n",
    "\n",
    "    def _issue_members(self, role: str = \"Inspeção-Execução\") -> dict:\n",
    "        return {\n",
    "            member[\"user\"][\"id\"]: member[\"user\"][\"name\"]\n",
    "            for member in self.project_members\n",
    "            if role in Issue.extract_value(member[\"roles\"]) and \"user\" in member\n",
    "        }\n",
    "\n",
    "    def _extract_acao(self) -> dict:\n",
    "        \"\"\"\n",
    "        Formats the relations of an issue as a dictionary.\n",
    "\n",
    "        Returns:\n",
    "            dict: A dictionary where the keys are the relation types, and the values are dictionaries containing the type, status, name, and description of the related issue.\n",
    "        \"\"\"\n",
    "        for k, v in self.relations().items():\n",
    "            if (type := getattr(v, \"type\", \"\")) == \"acao_de_inspecao\":\n",
    "                return {\n",
    "                    \"type\": type,\n",
    "                    \"status\": Issue.extract_value(v._attrs.get(\"status\")),\n",
    "                    \"name\": v._attrs.get(\"subject\"),\n",
    "                    \"description\": v._attrs.get(\"description\"),\n",
    "                }\n",
    "        return {}\n",
    "\n",
    "    def _fiscais2ids(self, fiscais: list) -> list:\n",
    "        id_fiscais = []\n",
    "        for fiscal in listify(fiscais):\n",
    "            if id_fiscal := self.names2id().get(fiscal):\n",
    "                id_fiscais.append(id_fiscal)\n",
    "        return id_fiscais\n",
    "\n",
    "    @cached_property\n",
    "    def _attrs(self) -> dict:\n",
    "        _ = list(self._issue)\n",
    "        self._issue.project.refresh()  # Prevent partial attrs return\n",
    "        try:\n",
    "            return dict(list(self._issue))\n",
    "        except Exception as e:\n",
    "            raise Exception(\n",
    "                f\"Não foi possível obter os atributos da issue {self.id}\"\n",
    "            ) from e\n",
    "\n",
    "    def custom_fields(self) -> dict:\n",
    "        return {\n",
    "            self._utf2ascii(field[\"name\"]): field\n",
    "            for field in self._attrs.get(\"custom_fields\", [])\n",
    "        }\n",
    "\n",
    "    @staticmethod\n",
    "    def extract_coords(attrs: dict) -> dict:\n",
    "        if coords := attrs.pop(\"coordenadas_geograficas\", None):\n",
    "            coords = Issue.__format_json_string(coords)\n",
    "            lat, long = coords.get(\"latitude\"), coords.get(\"longitude\")\n",
    "            try:\n",
    "                lat, long = float(lat), float(long)\n",
    "            except ValueError:\n",
    "                pass\n",
    "            attrs |= {\n",
    "                \"latitude_coordenadas\": str(lat),\n",
    "                \"longitude_coordenadas\": str(long),\n",
    "            }\n",
    "        if coords := attrs.pop(\"coordenadas_estacao\", None):\n",
    "            coords = Issue.__format_json_string(coords)\n",
    "            lat, long = coords.get(\"latitude\"), coords.get(\"longitude\")\n",
    "            try:\n",
    "                lat, long = float(lat), float(long)\n",
    "            except ValueError:\n",
    "                pass\n",
    "            attrs |= {\n",
    "                \"latitude_da_estacao\": str(lat),\n",
    "                \"longitude_da_estacao\": str(long),\n",
    "            }\n",
    "        return attrs\n",
    "\n",
    "    @staticmethod\n",
    "    def format_number_field(attrs: dict) -> dict:\n",
    "        \"\"\"\n",
    "        Formats empty values in a number field to 0\n",
    "        \"\"\"\n",
    "        for k in attrs:\n",
    "            if \"horas_\" in k and attrs[k] == \"\":\n",
    "                attrs[k] = 0\n",
    "            if attrs[k] is None:\n",
    "                attrs[k] = \"\"\n",
    "        return attrs\n",
    "\n",
    "    @cached_property\n",
    "    def attrs(self) -> dict:\n",
    "        \"\"\"Retrieves the attributes of an issue as a dictionary.\"\"\"\n",
    "        special_fields = [\"relations\", \"attachments\", \"custom_fields\", \"journals\"]\n",
    "        attrs = {}\n",
    "        for k, v in self._attrs.items():\n",
    "            if k in special_fields:\n",
    "                continue\n",
    "            attrs[k] = self.extract_value(v)\n",
    "\n",
    "        attrs.update(\n",
    "            {\n",
    "                k: self.extract_value(v.get(\"value\"))\n",
    "                for k, v in self.custom_fields().items()\n",
    "            }\n",
    "        )\n",
    "        attrs[\"anexos\"] = [\n",
    "            file[\"content_url\"] for file in self._attrs.get(\"attachments\", [])\n",
    "        ]\n",
    "        attrs[\"acao\"] = self._extract_acao()\n",
    "        attrs[\"atualizacao\"] = self.update_on()\n",
    "        attrs[\"membros\"] = list(self._issue_members().values())\n",
    "        attrs[\"no_fiscaliza_issue\"] = {\n",
    "            \"numero\": self.id,\n",
    "            \"link_acesso\": f\"{self.client.url}/issues/{self.id}\",\n",
    "        }\n",
    "        if fiscal := attrs.get(\"fiscal_responsavel\"):\n",
    "            attrs[\"fiscal_responsavel\"] = self.ids2names().get(int(fiscal), \"\")\n",
    "        attrs[\"fiscais\"] = [\n",
    "            self.ids2names().get(int(f), \"\") for f in listify(attrs.get(\"fiscais\", []))\n",
    "        ]\n",
    "        attrs = self.extract_coords(attrs)\n",
    "        attrs = self.format_number_field(attrs)\n",
    "        return {k: attrs[k] for k in sorted(attrs)}\n",
    "\n",
    "    @staticmethod\n",
    "    def _append_irregularity_options(editable_fields: dict) -> dict:\n",
    "        \"\"\"\n",
    "        Appends the options of the 'irregularidade' field to the editable_fields dictionary.\n",
    "        \"\"\"\n",
    "        if (\n",
    "            \"irregularidade\" in editable_fields\n",
    "        ):  # checking only the existence of the key, because it can be an empty list\n",
    "            match editable_fields[\"tipo_de_inspecao\"].value:\n",
    "                case \"Certificação\":\n",
    "                    options = [\n",
    "                        \"Comercialização de produtos\",\n",
    "                        \"Utilização de produtos\",\n",
    "                    ]\n",
    "                case \"Outorga - Aspectos não Técnicos\":\n",
    "                    options = [\n",
    "                        \"Conteúdo\",\n",
    "                        \"Outros aspectos não técnicos\",\n",
    "                        \"Recursos de acessibilidade\",\n",
    "                    ]\n",
    "                case \"Outorga - Aspectos Técnicos\":\n",
    "                    options = [\n",
    "                        \"Frequência diversa da autorizada\",\n",
    "                        \"Local da estação diverso do autorizado\",\n",
    "                        \"Outras irregularidades técnicas (especificar)\",\n",
    "                        \"Potência diversa da autorizada\",\n",
    "                    ]\n",
    "                case \"Uso do Espectro - Não Outorgado\":\n",
    "                    options = [\"Entidade não outorgada\"]\n",
    "                case __:\n",
    "                    options = []\n",
    "\n",
    "            editable_fields[\"irregularidade\"].options = options\n",
    "        return editable_fields\n",
    "\n",
    "    # @staticmethod\n",
    "    # def _cast_and_set(field, value):\n",
    "    #     try:\n",
    "    #         match field.dtype:\n",
    "    #             case \"string\":\n",
    "    #                 value = str(value)\n",
    "    #             case \"int\":\n",
    "    #                 value = int(value)\n",
    "    #             case \"float\":\n",
    "    #                 value = float(value)\n",
    "    #             case \"list\":\n",
    "    #                 value = listify(value)\n",
    "    #             case _:\n",
    "    #                 print(f\"Unknown dtype {field.dtype}, casting skipped...\")\n",
    "    #     except ValueError as e:\n",
    "    #         print(\n",
    "    #             f\"Error casting {field.name} value {value} to dtype {field.dtype}: {e}\"\n",
    "    #         )\n",
    "    #     setattr(field, \"value\", value)\n",
    "    #     return field\n",
    "\n",
    "    @cached_property\n",
    "    def editable_fields(self) -> dict:\n",
    "        \"\"\"Retrieves the editable fields of an issue as a dictionary.\"\"\"\n",
    "        editable_fields = {}\n",
    "        keys_by_id = sorted(FIELDS.keys(), key=lambda x: getattr(FIELDS[x], \"id\", 0))\n",
    "        fields = {k: FIELDS[k].reset(FIELDS[k]) for k in keys_by_id}\n",
    "        for key in self.attrs:\n",
    "            if field := fields.get(key):\n",
    "                if hasattr(field, \"options\"):\n",
    "                    if field.multiple:\n",
    "                        self.attrs[key] = [str(k) for k in self.attrs[key]]\n",
    "                    else:\n",
    "                        self.attrs[key] = str(self.attrs[key])\n",
    "                setattr(field, \"value\", self.attrs[key])\n",
    "                if key == \"fiscais\":\n",
    "                    setattr(field, \"options\", self.attrs[\"membros\"])\n",
    "                elif key == \"fiscal_responsavel\":\n",
    "                    setattr(field, \"options\", [\"\"] + self.attrs[\"membros\"])\n",
    "                editable_fields[key] = field\n",
    "        editable_fields = self._append_irregularity_options(editable_fields)\n",
    "        return self._update_fields(self.attrs, editable_fields)\n",
    "\n",
    "    def mandatory_fields(self) -> dict:\n",
    "        return {\n",
    "            k: v\n",
    "            for k, v in self.editable_fields.items()\n",
    "            if getattr(v, \"mandatory\", False)\n",
    "        }\n",
    "\n",
    "    def conditional_fields(self) -> dict:\n",
    "        return {\n",
    "            k: v\n",
    "            for k, v in self.editable_fields.items()\n",
    "            if getattr(v, \"mapping\", False)\n",
    "        }\n",
    "\n",
    "    @staticmethod\n",
    "    def _fields_derived_from_select_conditional(key, value) -> dict:\n",
    "        \"\"\"\n",
    "        Fill in the fields resulted from values on other fields.\n",
    "        \"\"\"\n",
    "        dependent_fields = {}\n",
    "        if field := CONDITIONAL_FIELDS.get(key):\n",
    "            for val in listify(\n",
    "                value\n",
    "            ):  # abusing use of empty defaults from dict.get to avoid if clauses\n",
    "                val = str(val)\n",
    "                if field.options:\n",
    "                    assert (\n",
    "                        val in field.options\n",
    "                    ), f\"Opção inválida para o campo {key}: {val}\"\n",
    "                for new_key in field.mapping.get(val, []):\n",
    "                    if new_key not in dependent_fields:\n",
    "                        dependent_fields[new_key] = FIELDS[new_key].reset(\n",
    "                            FIELDS[new_key]\n",
    "                        )\n",
    "\n",
    "        return dependent_fields\n",
    "\n",
    "    @staticmethod\n",
    "    def _keys_unrelated_from_select_conditional(key, value) -> dict:\n",
    "        \"\"\"\n",
    "        Fill in the fields resulted from values on other fields.\n",
    "        \"\"\"\n",
    "        unrelated_fields = set()\n",
    "        if field := CONDITIONAL_FIELDS.get(key):\n",
    "            for val in listify(\n",
    "                value\n",
    "            ):  # abusing use of empty defaults from dict.get to avoid if clauses\n",
    "                val = str(val)\n",
    "                if field.options:\n",
    "                    assert (\n",
    "                        val in field.options\n",
    "                    ), f\"Opção inválida para o campo {key}: {val}\"\n",
    "                for options in field.mapping.values():\n",
    "                    for option in options:\n",
    "                        if option not in field.mapping.get(val, []):\n",
    "                            unrelated_fields.add(option)\n",
    "        return unrelated_fields\n",
    "\n",
    "    @staticmethod\n",
    "    def _update_options_for_each_conditional(dados: dict) -> tuple[dict, set]:\n",
    "        dependent_fields = {}\n",
    "        unrelated_keys = set()\n",
    "        for key, value in dados.items():\n",
    "            dependent_fields |= Issue._fields_derived_from_select_conditional(\n",
    "                key, value\n",
    "            )\n",
    "            unrelated_keys.difference_update(dependent_fields)\n",
    "            unrelated_keys.update(\n",
    "                Issue._keys_unrelated_from_select_conditional(key, value)\n",
    "            )\n",
    "        return dependent_fields, unrelated_keys\n",
    "\n",
    "    @staticmethod\n",
    "    def _update_fields(dados: dict, editable_fields: dict) -> dict:\n",
    "        \"\"\"\n",
    "        Check if the data to be submitted to the Fiscaliza server is complete and valid.\n",
    "        \"\"\"\n",
    "        insert, delete = Issue._update_options_for_each_conditional(dados)\n",
    "        for key, value in insert.items():\n",
    "            if key not in editable_fields:\n",
    "                editable_fields[key] = value\n",
    "        for key in delete:\n",
    "            if key in editable_fields:\n",
    "                del editable_fields[key]\n",
    "        return editable_fields\n",
    "\n",
    "    def update_fields(self, dados: dict):\n",
    "        \"\"\"\n",
    "        Check if the data to be submitted to the Fiscaliza server is complete and valid.\n",
    "        \"\"\"\n",
    "        self.editable_fields = self._update_fields(dados, self.editable_fields)\n",
    "\n",
    "        for key, value in dados.items():\n",
    "            if key in self.editable_fields:\n",
    "                self.editable_fields[key](value)\n",
    "\n",
    "        self.editable_fields = self._append_irregularity_options(self.editable_fields)\n",
    "\n",
    "    def _get_id_only_fields(self, data: dict) -> dict:\n",
    "        if status := data.get(\"status\"):\n",
    "            data[\"status\"] = STATUS.get(status)\n",
    "        if fiscais := data.get(\"fiscais\"):\n",
    "            if id_fiscais := self._fiscais2ids(fiscais):\n",
    "                data[\"fiscais\"] = id_fiscais\n",
    "        if fiscal_responsavel := data.get(\"fiscal_responsavel\"):\n",
    "            if id_fiscal_responsavel := self.names2id().get(fiscal_responsavel):\n",
    "                data[\"fiscal_responsavel\"] = id_fiscal_responsavel\n",
    "        return data\n",
    "\n",
    "    def _parse_special_fields(self, data: dict) -> dict:\n",
    "        if (\n",
    "            (\"latitude_coordenadas\" in data) and (\"longitude_coordenadas\" in data)\n",
    "        ):  # Don't use numeric data that could be zero in clauses, that why the 'in' is here and not := dados.get(...)\n",
    "            newkey = \"coordenadas_geograficas\"\n",
    "            self.editable_fields[newkey] = SPECIAL_FIELDS[newkey].reset(\n",
    "                SPECIAL_FIELDS[newkey]\n",
    "            )\n",
    "            self.editable_fields.pop(\"latitude_coordenadas\", None)\n",
    "            self.editable_fields.pop(\"longitude_coordenadas\", None)\n",
    "            data[newkey] = (\n",
    "                data.pop(\"latitude_coordenadas\"),\n",
    "                data.pop(\"longitude_coordenadas\"),\n",
    "            )\n",
    "\n",
    "        elif (\"latitude_coordenadas\" in data) != (\"longitude_coordenadas\" in data):\n",
    "            raise ValueError(\n",
    "                \"Tanto 'latitude_coordenadas' quanto 'longitude_coordenadas' devem ser fornecidas juntas.\"\n",
    "            )\n",
    "        if (\"latitude_da_estacao\" in data) and (\"longitude_da_estacao\" in data):\n",
    "            newkey = \"coordenadas_estacao\"\n",
    "            self.editable_fields[newkey] = SPECIAL_FIELDS[newkey].reset(\n",
    "                SPECIAL_FIELDS[newkey]\n",
    "            )\n",
    "            self.editable_fields.pop(\"latitude_da_estacao\", None)\n",
    "            self.editable_fields.pop(\"longitude_da_estacao\", None)\n",
    "            data[newkey] = (\n",
    "                data.pop(\"latitude_da_estacao\"),\n",
    "                data.pop(\"longitude_da_estacao\"),\n",
    "            )\n",
    "\n",
    "        elif (\"latitude_da_estacao\" in data) != (\"longitude_da_estacao\" in data):\n",
    "            raise ValueError(\n",
    "                \"Tanto 'latitude_da_estacao' quanto 'longitude_da_estacao' devem ser fornecidas juntas.\"\n",
    "            )\n",
    "\n",
    "        newkey = \"gerar_plai\"\n",
    "\n",
    "        if newkey in data and data[newkey] == \"1\":\n",
    "            tipo_processo_plai = data.pop(\"tipo_do_processo_plai\", None)\n",
    "            coords_fi_plai = data.pop(\"coord_fi_plai\", None)\n",
    "            self.editable_fields.pop(\"tipo_do_processo_plai\", None)\n",
    "            self.editable_fields.pop(\"coord_fi_plai\", None)\n",
    "            if not all([tipo_processo_plai, coords_fi_plai]):\n",
    "                raise ValueError(\n",
    "                    \"Para gerar o PLAI é necessário fornecer o tipo do processo e as coordenação da FI\"\n",
    "                )\n",
    "            self.editable_fields[newkey] = SPECIAL_FIELDS[newkey].reset(\n",
    "                SPECIAL_FIELDS[newkey]\n",
    "            )\n",
    "            data[newkey] = (tipo_processo_plai, coords_fi_plai)\n",
    "\n",
    "        return data\n",
    "\n",
    "    def _validar_relatorio(self, dados):\n",
    "        \"\"\"Validates if the report file exists and is readable\"\"\"\n",
    "        if dados.get(\"gerar_relatorio\") == \"1\":\n",
    "            self.editable_fields[\"gerar_relatorio\"] = FIELDS[\"gerar_relatorio\"]\n",
    "            if (html_path := dados.get(\"html_path\")) is None:\n",
    "                raise ValueError(\n",
    "                    \"Foi solicitado a criação de um relatório no entanto o caminho para o arquivo html não foi fornecido\"\n",
    "                )\n",
    "            html = Path(html_path)\n",
    "            if not html.is_file():\n",
    "                raise ValueError(f\"Arquivo {html_path} não existe ou não é um arquivo\")\n",
    "            try:\n",
    "                html_text = html.read_text(encoding=\"utf-8\")\n",
    "            except UnicodeDecodeError:\n",
    "                html_text = html.read_text(encoding=\"cp1252\")\n",
    "            dados[\"html\"] = html_text\n",
    "            self.editable_fields[\"html\"] = FIELDS[\"html\"]\n",
    "            dados[\"relatorio_de_atividades\"] = \"\"\n",
    "            self.editable_fields[\"relatorio_de_atividades\"] = FIELDS[\n",
    "                \"relatorio_de_atividades\"\n",
    "            ]\n",
    "        else:\n",
    "            self.editable_fields[\"gerar_relatorio\"] = FIELDS[\"gerar_relatorio\"]\n",
    "            dados[\"gerar_relatorio\"] = \"0\"\n",
    "        if (relatorio := dados.get(\"relatorio_de_atividades\")) is not None:\n",
    "            dados[\"relatorio_de_atividades\"] = relatorio\n",
    "            self.editable_fields[\"relatorio_de_atividades\"] = FIELDS[\n",
    "                \"relatorio_de_atividades\"\n",
    "            ]\n",
    "        return dados\n",
    "\n",
    "    def _check_uploads(self, dados: dict) -> list:\n",
    "        uploads = []\n",
    "        if (files := dados.get(\"uploads\")) is not None:\n",
    "            files = listify(files)\n",
    "            for file in files:\n",
    "                if not isinstance(file, dict):\n",
    "                    raise ValueError(\n",
    "                        \"Os arquivos para upload devem ser fornecidos em um dicionário\"\n",
    "                    )\n",
    "                if (path := file.get(\"path\")) is not None:\n",
    "                    if not Path(path).is_file():\n",
    "                        raise ValueError(\n",
    "                            f\"Arquivo {path} não existe ou não é um arquivo\"\n",
    "                        )\n",
    "                    uploads.append(file)\n",
    "        return uploads\n",
    "\n",
    "    def _check_submission(self, dados: dict):\n",
    "        data = copy.deepcopy(dados)\n",
    "        self.update_fields(data)\n",
    "        data = self._validar_relatorio(data)\n",
    "        data = self._parse_special_fields(data)\n",
    "        data = {k: v for k, v in data.items() if k in self.editable_fields}\n",
    "        data = self._get_id_only_fields(data)\n",
    "        return data\n",
    "\n",
    "    def _parse_value_dict(self, dados: dict) -> dict:\n",
    "        data = self._check_submission(dados)\n",
    "        editable_fields = copy.deepcopy(self.editable_fields)\n",
    "        for key in [\"fiscais\", \"fiscal_responsavel\"]:\n",
    "            setattr(editable_fields[key], \"options\", None)\n",
    "        data = {k: editable_fields[k](v) for k, v in data.items()}\n",
    "        submitted_fields = {\"custom_fields\": []}\n",
    "        if uploads := self._check_uploads(dados):\n",
    "            submitted_fields[\"uploads\"] = uploads\n",
    "        for key, value in data.items():\n",
    "            if isinstance(editable_fields[key], AtomicField):\n",
    "                submitted_fields[editable_fields[key].keyword] = value\n",
    "            else:\n",
    "                submitted_fields[\"custom_fields\"].append(value)\n",
    "        return submitted_fields\n",
    "\n",
    "    def refresh(self) -> None:\n",
    "        \"\"\"Refreshes the issue's attributes.\"\"\"\n",
    "        if hasattr(self, \"editable_fields\"):\n",
    "            del self.editable_fields\n",
    "        if hasattr(self, \"_attrs\"):\n",
    "            del self._attrs\n",
    "        if hasattr(self, \"attrs\"):\n",
    "            del self.attrs\n",
    "        self._issue = self.client.issue.get(\n",
    "            self.id,\n",
    "            include=[\n",
    "                \"relations\",\n",
    "                \"attachments\",\n",
    "                \"journals\",\n",
    "                \"allowed_statuses\",\n",
    "            ],\n",
    "        )\n",
    "\n",
    "    def update(self, dados: dict) -> str:\n",
    "        \"\"\"Updates an issue with the given data.\"\"\"\n",
    "        self.refresh()\n",
    "        status = self.editable_fields[\"status\"].value\n",
    "        message = \"\"\n",
    "        for new_status in FLOW[status]:\n",
    "            status_id = STATUS[new_status]\n",
    "            if subset := STATES.get(new_status):\n",
    "                subset = {k: v for k, v in dados.items() if k in subset}\n",
    "                data = self._parse_value_dict(subset)\n",
    "                self.client.issue.update(self.id, status_id=status_id, **data)\n",
    "            else:\n",
    "                data = self._parse_value_dict(dados)\n",
    "                self.client.issue.update(self.id, status_id=status_id, **data)\n",
    "            message = f'A Inspeção nº {self.id} foi atualizada. O seu estado atual é \"{new_status}\".'\n",
    "            self.refresh()\n",
    "\n",
    "        return message"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "import nbdev; nbdev.nbdev_export()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
