@startuml
title Filing / Collection モジュール クラス関連図

skinparam packageStyle rectangle
skinparam classAttributeIconSize 0
skinparam linetype ortho

package "Fino Filing" {
  package "Collection Boundary" {
    class Collection <<Facade>> {
      - _storage : Storage
      - _catalog : Catalog
      - _locator : Locator
      + add(filing: Filing, content: bytes) : tuple[Filing, str]
      + get(id: str) : tuple[Filing | None, bytes | None, str | None]
      + get_filing(id: str) : Filing | None
      + get_content(id: str) : bytes | None
      + get_path(id: str) : str | None
      + search(expr: Expr | None, limit, offset, order_by, desc) : list[Filing]
    }

    class Catalog <<Repository>> {
      + db_path : str
      + conn : Connection
      - _resolver : FilingResolver
      + index(filing: Filing) : void
      + index_batch(filings: list[Filing]) : void
      + get(id: str) : Filing | None
      + get_raw(id: str) : dict | None
      + search(expr, limit, offset, order_by, desc) : list[Filing]
      + search_raw(sql: str, params) : list
      + count(expr: Expr | None) : int
      + stats() : dict
      + clear() : void
      + close() : void
    }

    class FilingResolver {
      - _registry : dict[str, type[Filing]]
      + register(cls: type[Filing]) : void
      + resolve(name: str | None) : type[Filing] | None
    }

    class Locator <<Strategy>> {
      + resolve(filing: Filing | None) : str | None
    }

    interface Storage <<Protocol>> {
      + base_dir : Path
      + save(content: bytes, storage_key: str | None) : str
      + load_by_path(relative_path: str) : bytes
    }

    class LocalStorage {
      + base_dir : Path
      + save(content: bytes, storage_key: str | None) : str
      + load_by_path(relative_path: str) : bytes
    }
  
    Collection o-- Storage : _storage
    Collection o-- Catalog : _catalog
    Collection o-- Locator : _locator

    Catalog o-- FilingResolver : _resolver
    LocalStorage .u.|> Storage : implements
  }

  package "Filing Boundary" {
    class Filing <<Document>> {
      - _data : dict
      $ _fields : dict
      $ _defaults : dict
      $ _core_fields : list[str]
      --
      core: id, source, checksum, name, is_zip, format, created_at
      --
      + to_dict() : dict[str, Any]
      + from_dict(data: dict) : Self
      + get(key: str) : Any
      + {class} get_indexed_fields() : list[str]
    }

    class FilingMeta <<Metaclass>> {
      + {static} __new__(mcs, name, bases, attrs) : type
    }

    class Field <<Descriptor>> {
      + name : str
      - _field_type : type | None
      + indexed : bool
      + immutable : bool
      + required : bool
      + identifier : bool
      + description : str | None
      --
      + validate_value(value: Any) : void
      + __eq__(value) : Expr
      + __ne__(value) : Expr
      + __gt__(value) : Expr
      + __ge__(value) : Expr
      + __lt__(value) : Expr
      + __le__(value) : Expr
      + contains(value: str) : Expr
      + startswith(value: str) : Expr
      + endswith(value: str) : Expr
      + in_(values: list) : Expr
      + not_in(values: list) : Expr
      + is_null() : Expr
      + is_not_null() : Expr
      + between(lower, upper) : Expr
    }

    class Expr {
      + sql : str
      + params : list[Any]
      --
      + __and__(other: Expr) : Expr
      + __or__(other: Expr) : Expr
      + __invert__() : Expr
    }
    class EDINETFiling {
      source = "EDINET"
      doc_id, edinet_code, sec_code, jcn, filer_name, ...
    }
    class EDGARFiling {
      cik, accession_number, company_name, form_type, ...
    }

    Filing --> FilingMeta : metaclass
    Filing <|-- EDINETFiling : extends
    Filing <|-- EDGARFiling : extends
    Filing o-- Field : _fields
    Field 0-- Expr
  }

  Catalog --> Filing : index, get, search
  FilingResolver ..> Filing : resolve() -> type
  Locator --> Filing : resolve(filing)

  Catalog --> Expr : search(expr)

  package "Collector Boundary" {
    abstract class BaseCollector <<Template Method>> {
      - collection : Collection
      --
      + collect() : void
      + add_to_collection(filing: Filing, content: bytes) : tuple[Filing, str]
      --
      # fetch_documents() : list[RawDocument]
      # parse_response(raw) : Parsed
      # build_filing(parsed) : Filing
    }

    class EdinetCollector {
      - client : EdinetClient
      --
      EdinetConfig のみ受け取り内部で Client を生成
      # fetch_documents() : list[RawDocument]
      # parse_response(raw) : Parsed
      # build_filing(parsed, raw) : EDINETFiling
    }

    class EdgerFactsCollector {
     - client : EdgerClient
      --
      用途: ファクト・概念など構造化データ
      # fetch_documents() : list[RawDocument]
      # parse_response(raw) : Parsed
      # build_filing(parsed) : Filing
    }

    class EdgerDocumentsCollector {
      - client : EdgerClient
      --
      用途: 提出書類(htm/iXBRL)
      # fetch_documents() : list[RawDocument]
      # parse_response(raw) : Parsed
      # build_filing(parsed) : EDGARFiling
    }

    class EdgerBulkCollector {
      - client : EdgerClient
      --
      用途: 一括取得
      # fetch_documents() : list[RawDocument]
      # parse_response(raw) : Parsed
      # build_filing(parsed) : EDGARFiling
    }

    class EdinetClient <<Client>> {
      - config : EdinetConfig
      --
      書類一覧API・書類取得APIのリクエスト実行と共通処理
      + get_document_list(date: str, type: int) : dict
      + get_document(doc_id: str) : bytes
    }

    class EdinetConfig {
      + api_key : str
      + timeout : int = 30
      --
    }

    class EdgerClient <<Client>> {
      - config : EdgerConfig
      --
      EDGAR 全エンドポイント対応の共通 HTTP クライアント
      レート制限・リトライを内部管理
      + get_submissions(cik: str) : dict
      + get_company_facts(cik: str) : dict
      + get_filing_document(cik, accession) : bytes
      + get_bulk(url: str) : bytes
    }

    class EdgerConfig {
      + user_agent_email : str
      + timeout : int = 0.1s
      --
    }

    BaseCollector <|-- EdinetCollector : extends
    BaseCollector <|-- EdgerFactsCollector : extends
    BaseCollector <|-- EdgerDocumentsCollector : extends
    BaseCollector <|-- EdgerBulkCollector : extends
    BaseCollector o-- Collection : collection
    EdinetCollector o-- EdinetClient : client
    EdgerFactsCollector o-- EdgerClient : client
    EdgerDocumentsCollector o-- EdgerClient : client
    EdgerBulkCollector o-- EdgerClient : client
    EdinetClient o-- EdinetConfig : config
    EdgerClient o-- EdgerConfig : config
  }

  BaseCollector --> Collection : add_to_collection
  EdinetClient ..> EDINETFiling : to_filing

  package "Error Boundary" {
    class FinoFilingException <<Exception>> {
      + message : str
      + __init__(message: str)
    }

    package "Collection Error"{
      class CollectionChecksumMismatchError <<Exception>> {
        + filing_id : str
        + actual_checksum : str
        + expected_checksum : str
      }

      class LocatorPathResolutionError <<Exception>> {
        + filing : Filing | None
      }

      class CatalogRequiredValueError <<Exception>> {
        + field : str
        + value : Any
      }

      class CatalogAlreadyExistsError <<Exception>> {
        + filing_id : str
      }
    }

    package "Filing Error" {
      class FieldValidationError <<Exception>> {
        + field : str
        + expected_type : type
        + actual_type : type
      }

      class FieldImmutableError <<Exception>> {
        + field : str
        + current_value : Any
        + attempt_value : Any
      }

      class FilingValidationError <<Exception>> {
        + errors : list[str]
        + fields : list[str]
      }

      class FilingImmutableError <<Exception>> {
        + errors : list[str]
        + fields : list[str]
      }

      class FilingRequiredError <<Exception>> {
        + errors : list[str]
        + fields : list[str]
      }
    }
  }
}

@enduml
